@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.
Files changed (116) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +91 -9
  3. package/dist/cel-environment.d.ts.map +1 -1
  4. package/dist/cel-environment.js +6 -3
  5. package/dist/definition-registry.d.ts +5 -0
  6. package/dist/definition-registry.d.ts.map +1 -1
  7. package/dist/definition-registry.js +9 -0
  8. package/dist/index.d.ts +11 -3
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -2
  11. package/dist/invocation-contract.d.ts +58 -13
  12. package/dist/invocation-contract.d.ts.map +1 -1
  13. package/dist/invocation-contract.js +123 -46
  14. package/dist/loaded-types.d.ts +13 -0
  15. package/dist/loaded-types.d.ts.map +1 -1
  16. package/dist/manifest-loader.d.ts +8 -1
  17. package/dist/manifest-loader.d.ts.map +1 -1
  18. package/dist/manifest-loader.js +33 -6
  19. package/dist/migrations/driver.d.ts +69 -0
  20. package/dist/migrations/driver.d.ts.map +1 -0
  21. package/dist/migrations/driver.js +200 -0
  22. package/dist/migrations/entries/index.d.ts +3 -0
  23. package/dist/migrations/entries/index.d.ts.map +1 -0
  24. package/dist/migrations/entries/index.js +9 -0
  25. package/dist/migrations/entries/normalize-value-types.json +48 -0
  26. package/dist/migrations/entries/ref-slot-scalar-type.json +20 -0
  27. package/dist/migrations/entry-data.d.ts +25 -0
  28. package/dist/migrations/entry-data.d.ts.map +1 -0
  29. package/dist/migrations/entry-data.js +171 -0
  30. package/dist/migrations/index.d.ts +22 -0
  31. package/dist/migrations/index.d.ts.map +1 -0
  32. package/dist/migrations/index.js +19 -0
  33. package/dist/migrations/match.d.ts +162 -0
  34. package/dist/migrations/match.d.ts.map +1 -0
  35. package/dist/migrations/match.js +295 -0
  36. package/dist/migrations/patch.d.ts +68 -0
  37. package/dist/migrations/patch.d.ts.map +1 -0
  38. package/dist/migrations/patch.js +236 -0
  39. package/dist/migrations/provenance.d.ts +25 -0
  40. package/dist/migrations/provenance.d.ts.map +1 -0
  41. package/dist/migrations/provenance.js +118 -0
  42. package/dist/migrations/registry.d.ts +35 -0
  43. package/dist/migrations/registry.d.ts.map +1 -0
  44. package/dist/migrations/registry.js +35 -0
  45. package/dist/migrations/report.d.ts +28 -0
  46. package/dist/migrations/report.d.ts.map +1 -0
  47. package/dist/migrations/report.js +144 -0
  48. package/dist/migrations/types.d.ts +115 -0
  49. package/dist/migrations/types.d.ts.map +1 -0
  50. package/dist/migrations/types.js +24 -0
  51. package/dist/migrations/yaml-edit.d.ts +21 -0
  52. package/dist/migrations/yaml-edit.d.ts.map +1 -0
  53. package/dist/migrations/yaml-edit.js +245 -0
  54. package/dist/parse-loaded-file.d.ts +8 -0
  55. package/dist/parse-loaded-file.d.ts.map +1 -1
  56. package/dist/parse-loaded-file.js +22 -13
  57. package/dist/resolve-schema-type-refs.d.ts +29 -12
  58. package/dist/resolve-schema-type-refs.d.ts.map +1 -1
  59. package/dist/resolve-schema-type-refs.js +89 -16
  60. package/dist/schema-compat.d.ts +53 -9
  61. package/dist/schema-compat.d.ts.map +1 -1
  62. package/dist/schema-compat.js +245 -66
  63. package/dist/schema-region.d.ts +33 -0
  64. package/dist/schema-region.d.ts.map +1 -0
  65. package/dist/schema-region.js +45 -0
  66. package/dist/types.d.ts +17 -0
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/validate-cel-context.d.ts.map +1 -1
  69. package/dist/validate-cel-context.js +97 -15
  70. package/dist/validate-step-inputs.d.ts +8 -1
  71. package/dist/validate-step-inputs.d.ts.map +1 -1
  72. package/dist/validate-step-inputs.js +47 -2
  73. package/dist/validate-value-type-slots.d.ts +43 -0
  74. package/dist/validate-value-type-slots.d.ts.map +1 -0
  75. package/dist/validate-value-type-slots.js +142 -0
  76. package/dist/value-type-keyword.d.ts +60 -0
  77. package/dist/value-type-keyword.d.ts.map +1 -0
  78. package/dist/value-type-keyword.js +119 -0
  79. package/dist/yaml-source-edit.d.ts +56 -0
  80. package/dist/yaml-source-edit.d.ts.map +1 -0
  81. package/dist/yaml-source-edit.js +93 -0
  82. package/package.json +3 -3
  83. package/src/analyzer.ts +99 -6
  84. package/src/cel-environment.ts +6 -3
  85. package/src/definition-registry.ts +10 -0
  86. package/src/index.ts +34 -3
  87. package/src/invocation-contract.ts +134 -48
  88. package/src/loaded-types.ts +13 -0
  89. package/src/manifest-loader.ts +37 -6
  90. package/src/migrations/driver.ts +262 -0
  91. package/src/migrations/entries/index.ts +10 -0
  92. package/src/migrations/entries/normalize-value-types.json +48 -0
  93. package/src/migrations/entries/ref-slot-scalar-type.json +20 -0
  94. package/src/migrations/entry-data.ts +207 -0
  95. package/src/migrations/index.ts +22 -0
  96. package/src/migrations/match.ts +382 -0
  97. package/src/migrations/patch.ts +265 -0
  98. package/src/migrations/provenance.ts +167 -0
  99. package/src/migrations/registry.ts +40 -0
  100. package/src/migrations/report.ts +169 -0
  101. package/src/migrations/types.ts +144 -0
  102. package/src/migrations/yaml-edit.ts +260 -0
  103. package/src/parse-loaded-file.ts +31 -12
  104. package/src/resolve-schema-type-refs.ts +93 -16
  105. package/src/schema-compat.ts +277 -75
  106. package/src/schema-region.ts +46 -0
  107. package/src/types.ts +17 -0
  108. package/src/validate-cel-context.ts +117 -15
  109. package/src/validate-step-inputs.ts +56 -1
  110. package/src/validate-value-type-slots.ts +182 -0
  111. package/src/value-type-keyword.ts +135 -0
  112. package/src/yaml-source-edit.ts +106 -0
  113. package/dist/binary-slot.d.ts +0 -39
  114. package/dist/binary-slot.d.ts.map +0 -1
  115. package/dist/binary-slot.js +0 -58
  116. 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.57.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.13.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.72.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: `${m.kind}/${stepName}: inputs at '${issue.path}' do not satisfy ${issue.targetLabel}'s declared inputType: ${issue.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)) continue;
2335
- const expected = slot.schema["x-telo-type"] ?? slot.schema.type ?? "unknown";
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: "CEL_TYPE_ERROR",
2429
+ code: "CEL_TYPE_ARGUMENT_MISMATCH",
2339
2430
  source: SOURCE,
2340
- message: `${slot.resource.kind}/${slot.resource.name}: CEL at '${slot.path}' returns '${type}' but the field expects '${expected}'.`,
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
  }
@@ -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
- withStreamPropertiesSkipped,
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 { binaryKeyword, isBinarySlot, X_TELO_BINARY } from "./binary-slot.js";
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 {