@telorun/analyzer 0.56.1 → 0.58.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 (133) hide show
  1. package/dist/analyzer.d.ts +5 -0
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +232 -94
  4. package/dist/cel-environment.d.ts.map +1 -1
  5. package/dist/cel-environment.js +6 -3
  6. package/dist/definition-registry.d.ts +5 -0
  7. package/dist/definition-registry.d.ts.map +1 -1
  8. package/dist/definition-registry.js +9 -0
  9. package/dist/index.d.ts +14 -4
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +8 -3
  12. package/dist/invocation-contract.d.ts +21 -13
  13. package/dist/invocation-contract.d.ts.map +1 -1
  14. package/dist/invocation-contract.js +43 -46
  15. package/dist/loaded-types.d.ts +13 -0
  16. package/dist/loaded-types.d.ts.map +1 -1
  17. package/dist/manifest-loader.d.ts +8 -1
  18. package/dist/manifest-loader.d.ts.map +1 -1
  19. package/dist/manifest-loader.js +33 -6
  20. package/dist/manifest-visitor.d.ts +4 -0
  21. package/dist/manifest-visitor.d.ts.map +1 -1
  22. package/dist/manifest-visitor.js +3 -3
  23. package/dist/migrations/driver.d.ts +69 -0
  24. package/dist/migrations/driver.d.ts.map +1 -0
  25. package/dist/migrations/driver.js +200 -0
  26. package/dist/migrations/entries/index.d.ts +3 -0
  27. package/dist/migrations/entries/index.d.ts.map +1 -0
  28. package/dist/migrations/entries/index.js +9 -0
  29. package/dist/migrations/entries/normalize-value-types.json +48 -0
  30. package/dist/migrations/entries/ref-slot-scalar-type.json +20 -0
  31. package/dist/migrations/entry-data.d.ts +25 -0
  32. package/dist/migrations/entry-data.d.ts.map +1 -0
  33. package/dist/migrations/entry-data.js +171 -0
  34. package/dist/migrations/index.d.ts +22 -0
  35. package/dist/migrations/index.d.ts.map +1 -0
  36. package/dist/migrations/index.js +19 -0
  37. package/dist/migrations/match.d.ts +162 -0
  38. package/dist/migrations/match.d.ts.map +1 -0
  39. package/dist/migrations/match.js +295 -0
  40. package/dist/migrations/patch.d.ts +68 -0
  41. package/dist/migrations/patch.d.ts.map +1 -0
  42. package/dist/migrations/patch.js +236 -0
  43. package/dist/migrations/provenance.d.ts +25 -0
  44. package/dist/migrations/provenance.d.ts.map +1 -0
  45. package/dist/migrations/provenance.js +118 -0
  46. package/dist/migrations/registry.d.ts +35 -0
  47. package/dist/migrations/registry.d.ts.map +1 -0
  48. package/dist/migrations/registry.js +35 -0
  49. package/dist/migrations/report.d.ts +28 -0
  50. package/dist/migrations/report.d.ts.map +1 -0
  51. package/dist/migrations/report.js +144 -0
  52. package/dist/migrations/types.d.ts +115 -0
  53. package/dist/migrations/types.d.ts.map +1 -0
  54. package/dist/migrations/types.js +24 -0
  55. package/dist/migrations/yaml-edit.d.ts +21 -0
  56. package/dist/migrations/yaml-edit.d.ts.map +1 -0
  57. package/dist/migrations/yaml-edit.js +245 -0
  58. package/dist/module-file-claims.d.ts +65 -0
  59. package/dist/module-file-claims.d.ts.map +1 -0
  60. package/dist/module-file-claims.js +106 -0
  61. package/dist/parse-loaded-file.d.ts +8 -0
  62. package/dist/parse-loaded-file.d.ts.map +1 -1
  63. package/dist/parse-loaded-file.js +22 -13
  64. package/dist/resolve-schema-type-refs.d.ts +29 -12
  65. package/dist/resolve-schema-type-refs.d.ts.map +1 -1
  66. package/dist/resolve-schema-type-refs.js +89 -16
  67. package/dist/schema-compat.d.ts +35 -9
  68. package/dist/schema-compat.d.ts.map +1 -1
  69. package/dist/schema-compat.js +181 -54
  70. package/dist/schema-region.d.ts +33 -0
  71. package/dist/schema-region.d.ts.map +1 -0
  72. package/dist/schema-region.js +45 -0
  73. package/dist/types.d.ts +51 -0
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js +6 -0
  76. package/dist/validate-cel-context.d.ts.map +1 -1
  77. package/dist/validate-cel-context.js +29 -3
  78. package/dist/validate-include-placement.d.ts +26 -0
  79. package/dist/validate-include-placement.d.ts.map +1 -0
  80. package/dist/validate-include-placement.js +67 -0
  81. package/dist/validate-step-inputs.d.ts +8 -1
  82. package/dist/validate-step-inputs.d.ts.map +1 -1
  83. package/dist/validate-step-inputs.js +47 -2
  84. package/dist/validate-throws-coverage.d.ts.map +1 -1
  85. package/dist/validate-throws-coverage.js +15 -12
  86. package/dist/validate-value-type-slots.d.ts +43 -0
  87. package/dist/validate-value-type-slots.d.ts.map +1 -0
  88. package/dist/validate-value-type-slots.js +142 -0
  89. package/dist/value-type-keyword.d.ts +60 -0
  90. package/dist/value-type-keyword.d.ts.map +1 -0
  91. package/dist/value-type-keyword.js +119 -0
  92. package/dist/yaml-source-edit.d.ts +56 -0
  93. package/dist/yaml-source-edit.d.ts.map +1 -0
  94. package/dist/yaml-source-edit.js +93 -0
  95. package/package.json +4 -3
  96. package/src/analyzer.ts +276 -129
  97. package/src/cel-environment.ts +6 -3
  98. package/src/definition-registry.ts +10 -0
  99. package/src/index.ts +31 -3
  100. package/src/invocation-contract.ts +43 -48
  101. package/src/loaded-types.ts +13 -0
  102. package/src/manifest-loader.ts +37 -6
  103. package/src/manifest-visitor.ts +11 -3
  104. package/src/migrations/driver.ts +262 -0
  105. package/src/migrations/entries/index.ts +10 -0
  106. package/src/migrations/entries/normalize-value-types.json +48 -0
  107. package/src/migrations/entries/ref-slot-scalar-type.json +20 -0
  108. package/src/migrations/entry-data.ts +207 -0
  109. package/src/migrations/index.ts +22 -0
  110. package/src/migrations/match.ts +382 -0
  111. package/src/migrations/patch.ts +265 -0
  112. package/src/migrations/provenance.ts +167 -0
  113. package/src/migrations/registry.ts +40 -0
  114. package/src/migrations/report.ts +169 -0
  115. package/src/migrations/types.ts +144 -0
  116. package/src/migrations/yaml-edit.ts +260 -0
  117. package/src/module-file-claims.ts +168 -0
  118. package/src/parse-loaded-file.ts +31 -12
  119. package/src/resolve-schema-type-refs.ts +93 -16
  120. package/src/schema-compat.ts +218 -62
  121. package/src/schema-region.ts +46 -0
  122. package/src/types.ts +54 -0
  123. package/src/validate-cel-context.ts +36 -3
  124. package/src/validate-include-placement.ts +70 -0
  125. package/src/validate-step-inputs.ts +56 -1
  126. package/src/validate-throws-coverage.ts +16 -11
  127. package/src/validate-value-type-slots.ts +182 -0
  128. package/src/value-type-keyword.ts +135 -0
  129. package/src/yaml-source-edit.ts +106 -0
  130. package/dist/binary-slot.d.ts +0 -39
  131. package/dist/binary-slot.d.ts.map +0 -1
  132. package/dist/binary-slot.js +0 -58
  133. package/src/binary-slot.ts +0 -71
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Static validation of the `x-telo-type` annotation itself — the strict half of
3
+ * the accessor split, and the `validate-ref-slots.ts` precedent.
4
+ *
5
+ * `readValueTypeSlot` is deliberately lenient: it reports whatever it can read,
6
+ * returning a slot with no `entry` for a name it does not know. That leniency is
7
+ * what lets every surface keep working; this pass is what stops it becoming
8
+ * silence. Before the unification an unrecognized brand simply resolved to
9
+ * `undefined` and the slot quietly lost its identity — the same class of failure
10
+ * `X_TELO_REF_INVALID_USE` exists to prevent for `use` tokens.
11
+ *
12
+ * The vocabulary is CLOSED and `Telo.`-qualified, so there is nothing here to
13
+ * resolve against an alias scope: a name is a built-in or it is a mistake. A
14
+ * SHAPE is a different thing entirely and is named with the reference tag, which
15
+ * carries its own resolution and its own diagnostics — this pass never sees one,
16
+ * because `resolveSchemaTypeRefs` has already turned it into a `$ref`.
17
+ *
18
+ * Scoping follows `X_TELO_REF_UNRESOLVED`: reported only for manifests in the
19
+ * entry's own modules, since a published dependency is not the consumer's to fix.
20
+ *
21
+ * Browser-safe: no Node built-ins.
22
+ */
23
+ import { readValueTypeSlot, VALUE_TYPES, X_TELO_TYPE } from "@telorun/sdk";
24
+ import { distance } from "./levenshtein.js";
25
+ import { isInSchemaRegion } from "./schema-region.js";
26
+ /** Schema regions are reached by ANCESTRY, not by root key — see
27
+ * `schema-region.ts`. Walking a manifest's root fields covers only a fraction of
28
+ * the sites an author writes a schema at: an API route's `request.schema.body`
29
+ * sits under `routes`, and a check that never reaches it is a hole in exactly
30
+ * the diagnostic that exists to stop an unknown name degrading silently. */
31
+ /** The closest declared type name within an edit-distance threshold, or
32
+ * undefined. Mirrors `computeSuggestKind`: case-sensitive, and silent on a tie,
33
+ * because a coin-flip suggestion is worse than none. */
34
+ function suggestValueType(name) {
35
+ if (!name)
36
+ return undefined;
37
+ const threshold = Math.min(3, Math.floor(name.length / 3));
38
+ if (threshold < 1)
39
+ return undefined;
40
+ let best;
41
+ let bestDist = threshold + 1;
42
+ let tied = false;
43
+ for (const candidate of VALUE_TYPES.keys()) {
44
+ const d = distance(name, candidate);
45
+ if (d < bestDist) {
46
+ best = candidate;
47
+ bestDist = d;
48
+ tied = false;
49
+ }
50
+ else if (d === bestDist) {
51
+ tied = true;
52
+ }
53
+ }
54
+ return !best || bestDist > threshold || tied ? undefined : best;
55
+ }
56
+ function declaredNames() {
57
+ return [...VALUE_TYPES.keys()].join(", ");
58
+ }
59
+ /** Report the annotation on one schema node. */
60
+ function checkNode(node, manifest, path, issues) {
61
+ const slot = readValueTypeSlot(node);
62
+ if (!slot)
63
+ return;
64
+ if (!slot.entry) {
65
+ const suggestion = suggestValueType(slot.name);
66
+ // Only the bare-name spelling can carry the repair: it IS the annotation's
67
+ // whole value, which is the only shape `DiagnosticFix` describes.
68
+ const bareName = typeof node[X_TELO_TYPE] === "string";
69
+ issues.push({
70
+ code: "X_TELO_TYPE_UNKNOWN",
71
+ manifest,
72
+ path,
73
+ message: `'${slot.name || "(missing name)"}' is not a value type. ` +
74
+ (suggestion ? `Did you mean '${suggestion}'? ` : "") +
75
+ `Declared types: ${declaredNames()}. A value type names how a value is ` +
76
+ `REPRESENTED and is kernel-owned; to name a shape, reference it with !ref.`,
77
+ ...(suggestion && bareName ? { fix: { replacement: suggestion } } : {}),
78
+ });
79
+ return;
80
+ }
81
+ const declared = new Set(slot.entry.parameters.map((p) => p.name));
82
+ for (const argument of Object.keys(slot.args)) {
83
+ if (declared.has(argument))
84
+ continue;
85
+ issues.push({
86
+ code: "X_TELO_TYPE_ARGUMENT_UNKNOWN",
87
+ manifest,
88
+ path,
89
+ message: `'${slot.entry.name}' declares no type parameter '${argument}'. ` +
90
+ (declared.size > 0
91
+ ? `Its parameters: ${[...declared].join(", ")}.`
92
+ : `It takes no type parameters.`),
93
+ });
94
+ }
95
+ }
96
+ /** Walk a schema value, reporting every annotation it carries.
97
+ *
98
+ * Descends through every container rather than through a keyword list: a value
99
+ * type is legal at any schema position — a property, an item, a union branch, a
100
+ * `$defs` entry, a type argument — and enumerating positions is how a check
101
+ * ends up not covering the one an author used. */
102
+ function walk(value, manifest, path, segments, seen, issues) {
103
+ if (value === null || typeof value !== "object")
104
+ return;
105
+ if (seen.has(value))
106
+ return;
107
+ seen.add(value);
108
+ if (Array.isArray(value)) {
109
+ value.forEach((item, i) => walk(item, manifest, `${path}[${i}]`, [...segments, i], seen, issues));
110
+ return;
111
+ }
112
+ const node = value;
113
+ // Report only inside a schema region. The walk itself is unbounded — reporting
114
+ // is safe anywhere, unlike a rewrite — but an `x-telo-type` key sitting in a
115
+ // resource's own configuration is not a schema annotation and is not this
116
+ // check's to judge.
117
+ if (isInSchemaRegion([...segments, X_TELO_TYPE])) {
118
+ checkNode(node, manifest, path, issues);
119
+ }
120
+ for (const [key, child] of Object.entries(node)) {
121
+ // The annotation's own value is read by `checkNode`; descending into it
122
+ // would report the type ARGUMENTS as if they were annotated nodes of their
123
+ // own. Their turn comes below, as ordinary schema nodes.
124
+ if (key === X_TELO_TYPE) {
125
+ // Walk the NORMALIZED arguments, so a bare-name argument (`of: Telo.Bytes`)
126
+ // is checked exactly as its expanded form is — the sugar must not be a
127
+ // hole in the check that exists to catch a misspelled name.
128
+ const slot = readValueTypeSlot(node);
129
+ for (const [argName, argValue] of Object.entries(slot?.args ?? {})) {
130
+ walk(argValue, manifest, `${path}.${X_TELO_TYPE}.${argName}`, [...segments, X_TELO_TYPE, argName], seen, issues);
131
+ }
132
+ continue;
133
+ }
134
+ walk(child, manifest, path ? `${path}.${key}` : key, [...segments, key], seen, issues);
135
+ }
136
+ }
137
+ /** Every `x-telo-type` problem in one manifest, wherever a schema is written. */
138
+ export function validateValueTypeSlots(manifest) {
139
+ const issues = [];
140
+ walk(manifest, manifest, "", [], new Set(), issues);
141
+ return issues;
142
+ }
@@ -0,0 +1,60 @@
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 type { KeywordDefinition } from "ajv";
24
+ /**
25
+ * Annotations that carry no validation at all: analyzer, editor and topology
26
+ * metadata. Registered as no-ops so AJV does not treat them as unknown keywords,
27
+ * and listed in one place so an instance cannot know about half of them.
28
+ *
29
+ * `x-telo-type` is deliberately absent — it is the one that emits code.
30
+ */
31
+ export declare const ANNOTATION_KEYWORDS: readonly ["x-telo-bindings-from", "x-telo-catches-for", "x-telo-context", "x-telo-context-element-from", "x-telo-context-from", "x-telo-context-from-ref-kind", "x-telo-context-from-root", "x-telo-context-ref-from", "x-telo-error-context", "x-telo-eval", "x-telo-inline", "x-telo-outcome-list", "x-telo-provides-zone", "x-telo-ref", "x-telo-requires-zone", "x-telo-schema-from", "x-telo-scope", "x-telo-step-context", "x-telo-topology-role", "x-telo-value-schema-from", "x-telo-widget"];
32
+ /**
33
+ * The `x-telo-type` keyword.
34
+ *
35
+ * Three postures, decided by the entry and never by this code:
36
+ *
37
+ * - a `json` representation validates through its own declared schema, so the
38
+ * keyword emits nothing — the name carries nominal identity for static wiring
39
+ * and has no runtime existence at all;
40
+ * - a `live` instance is EXEMPT: its value is never traversed, because iterating
41
+ * a stream to check it is precisely what the exemption is for;
42
+ * - every other instance is ASSERTED against the constructor its binding names.
43
+ *
44
+ * An unknown name emits nothing here. It is a hard diagnostic in the analyzer
45
+ * (`X_TELO_TYPE_UNKNOWN`), which is where a name can be reported against the
46
+ * manifest that wrote it; failing compilation instead would take out every
47
+ * validator in a module for one typo in one slot.
48
+ */
49
+ export declare function valueTypeKeyword(): KeywordDefinition;
50
+ /**
51
+ * Register every Telo keyword on an AJV instance: the annotations as no-ops and
52
+ * `x-telo-type` as the one that checks.
53
+ *
54
+ * Every AJV instance in the runtime and the analyzer goes through this, so a
55
+ * schema means the same thing wherever it is validated.
56
+ */
57
+ export declare function registerTeloKeywords(ajv: {
58
+ addKeyword: (keyword: any, definition?: any) => unknown;
59
+ }): void;
60
+ //# sourceMappingURL=value-type-keyword.d.ts.map
@@ -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.56.1",
3
+ "version": "0.58.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -41,14 +41,15 @@
41
41
  "ajv": "^8.17.1",
42
42
  "ajv-formats": "^3.0.1",
43
43
  "jsonpath-plus": "^10.3.0",
44
+ "packageurl-js": "^2.0.1",
44
45
  "yaml": "^2.8.3",
45
- "@telorun/templating": "0.12.0"
46
+ "@telorun/templating": "0.14.0"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@types/node": "^20.0.0",
49
50
  "typescript": "^5.0.0",
50
51
  "vitest": "^2.1.8",
51
- "@telorun/sdk": "0.70.0"
52
+ "@telorun/sdk": "0.73.0"
52
53
  },
53
54
  "peerDependencies": {
54
55
  "@telorun/sdk": "*"