@telorun/analyzer 0.57.0 → 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 (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 +10 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -2
  11. package/dist/invocation-contract.d.ts +21 -13
  12. package/dist/invocation-contract.d.ts.map +1 -1
  13. package/dist/invocation-contract.js +43 -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 +35 -9
  61. package/dist/schema-compat.d.ts.map +1 -1
  62. package/dist/schema-compat.js +180 -64
  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 +29 -3
  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 +26 -2
  87. package/src/invocation-contract.ts +43 -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 +212 -74
  106. package/src/schema-region.ts +46 -0
  107. package/src/types.ts +17 -0
  108. package/src/validate-cel-context.ts +36 -3
  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,182 @@
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, type ResourceManifest } from "@telorun/sdk";
24
+ import { distance } from "./levenshtein.js";
25
+ import { isInSchemaRegion } from "./schema-region.js";
26
+
27
+ export interface ValueTypeSlotIssue {
28
+ code: "X_TELO_TYPE_UNKNOWN" | "X_TELO_TYPE_ARGUMENT_UNKNOWN";
29
+ manifest: ResourceManifest;
30
+ /** Dotted path to the annotated schema node, e.g. `schema.properties.body`. */
31
+ path: string;
32
+ message: string;
33
+ /** The whole-value replacement that repairs it, when one is derivable. A
34
+ * misspelled name has a single correct spelling and the annotation's value is
35
+ * that name, so the repair is the primitive `DiagnosticFix` already carries —
36
+ * computing a suggestion and printing it in prose alone leaves the author to
37
+ * retype what the analyzer already knows. Only for the bare-name spelling: the
38
+ * object form's name is nested, and a whole-value replacement there would
39
+ * discard the type arguments beside it. */
40
+ fix?: { replacement: string };
41
+ }
42
+
43
+ /** Schema regions are reached by ANCESTRY, not by root key — see
44
+ * `schema-region.ts`. Walking a manifest's root fields covers only a fraction of
45
+ * the sites an author writes a schema at: an API route's `request.schema.body`
46
+ * sits under `routes`, and a check that never reaches it is a hole in exactly
47
+ * the diagnostic that exists to stop an unknown name degrading silently. */
48
+
49
+ /** The closest declared type name within an edit-distance threshold, or
50
+ * undefined. Mirrors `computeSuggestKind`: case-sensitive, and silent on a tie,
51
+ * because a coin-flip suggestion is worse than none. */
52
+ function suggestValueType(name: string): string | undefined {
53
+ if (!name) return undefined;
54
+ const threshold = Math.min(3, Math.floor(name.length / 3));
55
+ if (threshold < 1) return undefined;
56
+ let best: string | undefined;
57
+ let bestDist = threshold + 1;
58
+ let tied = false;
59
+ for (const candidate of VALUE_TYPES.keys()) {
60
+ const d = distance(name, candidate);
61
+ if (d < bestDist) {
62
+ best = candidate;
63
+ bestDist = d;
64
+ tied = false;
65
+ } else if (d === bestDist) {
66
+ tied = true;
67
+ }
68
+ }
69
+ return !best || bestDist > threshold || tied ? undefined : best;
70
+ }
71
+
72
+ function declaredNames(): string {
73
+ return [...VALUE_TYPES.keys()].join(", ");
74
+ }
75
+
76
+ /** Report the annotation on one schema node. */
77
+ function checkNode(
78
+ node: Record<string, unknown>,
79
+ manifest: ResourceManifest,
80
+ path: string,
81
+ issues: ValueTypeSlotIssue[],
82
+ ): void {
83
+ const slot = readValueTypeSlot(node);
84
+ if (!slot) return;
85
+
86
+ if (!slot.entry) {
87
+ const suggestion = suggestValueType(slot.name);
88
+ // Only the bare-name spelling can carry the repair: it IS the annotation's
89
+ // whole value, which is the only shape `DiagnosticFix` describes.
90
+ const bareName = typeof (node as Record<string, unknown>)[X_TELO_TYPE] === "string";
91
+ issues.push({
92
+ code: "X_TELO_TYPE_UNKNOWN",
93
+ manifest,
94
+ path,
95
+ message:
96
+ `'${slot.name || "(missing name)"}' is not a value type. ` +
97
+ (suggestion ? `Did you mean '${suggestion}'? ` : "") +
98
+ `Declared types: ${declaredNames()}. A value type names how a value is ` +
99
+ `REPRESENTED and is kernel-owned; to name a shape, reference it with !ref.`,
100
+ ...(suggestion && bareName ? { fix: { replacement: suggestion } } : {}),
101
+ });
102
+ return;
103
+ }
104
+
105
+ const declared = new Set(slot.entry.parameters.map((p) => p.name));
106
+ for (const argument of Object.keys(slot.args)) {
107
+ if (declared.has(argument)) continue;
108
+ issues.push({
109
+ code: "X_TELO_TYPE_ARGUMENT_UNKNOWN",
110
+ manifest,
111
+ path,
112
+ message:
113
+ `'${slot.entry.name}' declares no type parameter '${argument}'. ` +
114
+ (declared.size > 0
115
+ ? `Its parameters: ${[...declared].join(", ")}.`
116
+ : `It takes no type parameters.`),
117
+ });
118
+ }
119
+ }
120
+
121
+ /** Walk a schema value, reporting every annotation it carries.
122
+ *
123
+ * Descends through every container rather than through a keyword list: a value
124
+ * type is legal at any schema position — a property, an item, a union branch, a
125
+ * `$defs` entry, a type argument — and enumerating positions is how a check
126
+ * ends up not covering the one an author used. */
127
+ function walk(
128
+ value: unknown,
129
+ manifest: ResourceManifest,
130
+ path: string,
131
+ segments: (string | number)[],
132
+ seen: Set<object>,
133
+ issues: ValueTypeSlotIssue[],
134
+ ): void {
135
+ if (value === null || typeof value !== "object") return;
136
+ if (seen.has(value)) return;
137
+ seen.add(value);
138
+ if (Array.isArray(value)) {
139
+ value.forEach((item, i) =>
140
+ walk(item, manifest, `${path}[${i}]`, [...segments, i], seen, issues),
141
+ );
142
+ return;
143
+ }
144
+ const node = value as Record<string, unknown>;
145
+ // Report only inside a schema region. The walk itself is unbounded — reporting
146
+ // is safe anywhere, unlike a rewrite — but an `x-telo-type` key sitting in a
147
+ // resource's own configuration is not a schema annotation and is not this
148
+ // check's to judge.
149
+ if (isInSchemaRegion([...segments, X_TELO_TYPE])) {
150
+ checkNode(node, manifest, path, issues);
151
+ }
152
+ for (const [key, child] of Object.entries(node)) {
153
+ // The annotation's own value is read by `checkNode`; descending into it
154
+ // would report the type ARGUMENTS as if they were annotated nodes of their
155
+ // own. Their turn comes below, as ordinary schema nodes.
156
+ if (key === X_TELO_TYPE) {
157
+ // Walk the NORMALIZED arguments, so a bare-name argument (`of: Telo.Bytes`)
158
+ // is checked exactly as its expanded form is — the sugar must not be a
159
+ // hole in the check that exists to catch a misspelled name.
160
+ const slot = readValueTypeSlot(node);
161
+ for (const [argName, argValue] of Object.entries(slot?.args ?? {})) {
162
+ walk(
163
+ argValue,
164
+ manifest,
165
+ `${path}.${X_TELO_TYPE}.${argName}`,
166
+ [...segments, X_TELO_TYPE, argName],
167
+ seen,
168
+ issues,
169
+ );
170
+ }
171
+ continue;
172
+ }
173
+ walk(child, manifest, path ? `${path}.${key}` : key, [...segments, key], seen, issues);
174
+ }
175
+ }
176
+
177
+ /** Every `x-telo-type` problem in one manifest, wherever a schema is written. */
178
+ export function validateValueTypeSlots(manifest: ResourceManifest): ValueTypeSlotIssue[] {
179
+ const issues: ValueTypeSlotIssue[] = [];
180
+ walk(manifest, manifest, "", [], new Set<object>(), issues);
181
+ return issues;
182
+ }
@@ -0,0 +1,135 @@
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
+
24
+ import * as AjvNS from "ajv";
25
+ import type { KeywordDefinition } from "ajv";
26
+ import {
27
+ VALUE_TYPE_BINDINGS,
28
+ X_TELO_TYPE,
29
+ readValueTypeSlot,
30
+ type ValueTypeEntry,
31
+ } from "@telorun/sdk";
32
+
33
+ // AJV's codegen template tag. The package is consumed in both ESM and CJS interop
34
+ // shapes, so the named export may sit on the namespace or behind `.default` —
35
+ // the same fallback `schema-compat` uses to reach the constructor. Skipping it
36
+ // would leave `codegen` undefined under a loader that does not detect AJV's named
37
+ // CJS re-exports, and the keyword would throw on its first compile.
38
+ type CodegenTag = (s: TemplateStringsArray, ...a: unknown[]) => unknown;
39
+ const ajvExports = (AjvNS as any).default ?? AjvNS;
40
+ const codegen: CodegenTag = ajvExports._ ?? (AjvNS as any)._;
41
+
42
+ /**
43
+ * Annotations that carry no validation at all: analyzer, editor and topology
44
+ * metadata. Registered as no-ops so AJV does not treat them as unknown keywords,
45
+ * and listed in one place so an instance cannot know about half of them.
46
+ *
47
+ * `x-telo-type` is deliberately absent — it is the one that emits code.
48
+ */
49
+ export const ANNOTATION_KEYWORDS = [
50
+ "x-telo-bindings-from",
51
+ "x-telo-catches-for",
52
+ "x-telo-context",
53
+ "x-telo-context-element-from",
54
+ "x-telo-context-from",
55
+ "x-telo-context-from-ref-kind",
56
+ "x-telo-context-from-root",
57
+ "x-telo-context-ref-from",
58
+ "x-telo-error-context",
59
+ "x-telo-eval",
60
+ "x-telo-inline",
61
+ "x-telo-outcome-list",
62
+ "x-telo-provides-zone",
63
+ "x-telo-ref",
64
+ "x-telo-requires-zone",
65
+ "x-telo-schema-from",
66
+ "x-telo-scope",
67
+ "x-telo-step-context",
68
+ "x-telo-topology-role",
69
+ "x-telo-value-schema-from",
70
+ "x-telo-widget",
71
+ ] as const;
72
+
73
+ /**
74
+ * The `x-telo-type` keyword.
75
+ *
76
+ * Three postures, decided by the entry and never by this code:
77
+ *
78
+ * - a `json` representation validates through its own declared schema, so the
79
+ * keyword emits nothing — the name carries nominal identity for static wiring
80
+ * and has no runtime existence at all;
81
+ * - a `live` instance is EXEMPT: its value is never traversed, because iterating
82
+ * a stream to check it is precisely what the exemption is for;
83
+ * - every other instance is ASSERTED against the constructor its binding names.
84
+ *
85
+ * An unknown name emits nothing here. It is a hard diagnostic in the analyzer
86
+ * (`X_TELO_TYPE_UNKNOWN`), which is where a name can be reported against the
87
+ * manifest that wrote it; failing compilation instead would take out every
88
+ * validator in a module for one typo in one slot.
89
+ */
90
+ export function valueTypeKeyword(): KeywordDefinition {
91
+ return {
92
+ keyword: X_TELO_TYPE,
93
+ // Both spellings: a bare name, or the object form carrying type arguments.
94
+ schemaType: ["string", "object"],
95
+ code(cxt: any) {
96
+ const entry: ValueTypeEntry | undefined = readValueTypeSlot({
97
+ [X_TELO_TYPE]: cxt.schema,
98
+ })?.entry;
99
+ if (!entry || entry.representation !== "instance" || entry.live) return;
100
+ const binding = VALUE_TYPE_BINDINGS[entry.binding!];
101
+ if (!binding) return;
102
+ // The constructor reaches generated code through AJV's value scope, which
103
+ // is what keeps this general: `Uint8Array` happens to be a global, but a
104
+ // binding may name a class that is not, and inlining a bare identifier
105
+ // would compile to a reference that does not resolve.
106
+ const ctor = cxt.gen.scopeValue("obj", {
107
+ ref: binding.constructor,
108
+ code: codegen`require("@telorun/sdk").VALUE_TYPE_BINDINGS[${entry.binding!}].constructor`,
109
+ });
110
+ cxt.pass(codegen`${cxt.data} instanceof ${ctor}`);
111
+ },
112
+ error: {
113
+ message: (cxt: any) => {
114
+ const entry = readValueTypeSlot({ [X_TELO_TYPE]: cxt.schema })?.entry;
115
+ return entry?.binding === "bytes"
116
+ ? "must be raw bytes (a Uint8Array) — bytes cannot be written inline in a manifest"
117
+ : `must be a ${entry?.name ?? "declared value type"} — this value is not writable inline in a manifest`;
118
+ },
119
+ },
120
+ } as KeywordDefinition;
121
+ }
122
+
123
+ /**
124
+ * Register every Telo keyword on an AJV instance: the annotations as no-ops and
125
+ * `x-telo-type` as the one that checks.
126
+ *
127
+ * Every AJV instance in the runtime and the analyzer goes through this, so a
128
+ * schema means the same thing wherever it is validated.
129
+ */
130
+ export function registerTeloKeywords(ajv: {
131
+ addKeyword: (keyword: any, definition?: any) => unknown;
132
+ }): void {
133
+ for (const keyword of ANNOTATION_KEYWORDS) ajv.addKeyword(keyword);
134
+ ajv.addKeyword(valueTypeKeyword());
135
+ }
@@ -0,0 +1,106 @@
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
+
21
+ /** Characters that make a plain (unquoted) YAML scalar reparse as something
22
+ * else. `-` and `?` are indicators only when followed by a space, so they are
23
+ * handled by the leading-token check rather than listed here. */
24
+ const PLAIN_UNSAFE_LEAD = new Set([
25
+ "&", "*", "!", "|", ">", "%", "@", "`", "#", "'", '"', "{", "[", "}", "]", ",",
26
+ ]);
27
+
28
+ /** Whether `value` can be written as a plain scalar without changing meaning. */
29
+ export function isPlainSafe(value: string): boolean {
30
+ if (value === "" || value.trim() !== value) return false;
31
+ if (PLAIN_UNSAFE_LEAD.has(value[0]!)) 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)) return false;
35
+ // A colon-space anywhere opens a mapping; a space-hash opens a comment.
36
+ if (value.includes(": ") || value.includes(" #")) return false;
37
+ if (value.endsWith(":")) return false;
38
+ return !/[\n\r]/.test(value);
39
+ }
40
+
41
+ /** Quote style of the source text a repair is replacing. */
42
+ export type QuoteStyle = "double" | "single" | "plain";
43
+
44
+ export function quoteStyleOf(source: string): QuoteStyle {
45
+ if (source.length >= 2 && source.startsWith('"') && source.endsWith('"')) return "double";
46
+ if (source.length >= 2 && source.startsWith("'") && source.endsWith("'")) return "single";
47
+ return "plain";
48
+ }
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(
68
+ originalSource: string,
69
+ replacement: string,
70
+ ): string | undefined {
71
+ if (/[\n\r]/.test(originalSource) || /[\n\r]/.test(replacement)) return undefined;
72
+ const style = quoteStyleOf(originalSource);
73
+
74
+ if (style === "single") {
75
+ // A single-quoted YAML scalar escapes only the quote, by doubling it. CEL
76
+ // string literals use single quotes constantly, so this is the common case
77
+ // for an expression written in a single-quoted scalar.
78
+ return `'${replacement.replaceAll("'", "''")}'`;
79
+ }
80
+ if (style === "double" || !isPlainSafe(replacement)) {
81
+ return `"${replacement.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
82
+ }
83
+ return replacement;
84
+ }
85
+
86
+ /** A splice over a source file: replace `[start, end)` with `newText`. An empty
87
+ * span is a pure insertion. */
88
+ export interface TextEdit {
89
+ /** Byte offset of the first replaced character (inclusive). */
90
+ readonly start: number;
91
+ /** Byte offset one past the last replaced character. */
92
+ readonly end: number;
93
+ readonly newText: string;
94
+ }
95
+
96
+ /** Splice `edits` into `text`, right to left so earlier offsets stay valid.
97
+ * Callers are responsible for edits not overlapping; two splices contending
98
+ * for the same bytes cannot both be honoured, and this does not arbitrate. */
99
+ export function applyTextEdits(text: string, edits: readonly TextEdit[]): string {
100
+ if (edits.length === 0) return text;
101
+ let out = text;
102
+ for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
103
+ out = out.slice(0, edit.start) + edit.newText + out.slice(edit.end);
104
+ }
105
+ return out;
106
+ }
@@ -1,39 +0,0 @@
1
- import type { KeywordDefinition } from "ajv";
2
- /**
3
- * `x-telo-binary: true` — the annotation that gives a byte buffer a declared
4
- * identity, and the single accessor every surface reads it through (the
5
- * `ref-slot.ts` precedent).
6
- *
7
- * WHY AN ANNOTATION RATHER THAN A TYPE. Bytes are not expressible in JSON
8
- * Schema's type vocabulary. `type: object` is the closest fit and it is a lie
9
- * that costs a real check: every arbitrary object satisfies it, so a mistyped
10
- * `content: { foo: bar }` reaches the controller instead of failing statically.
11
- * `type: binary` would fix the honesty and break everything else — AJV refuses
12
- * to compile an unknown type at all (`type must be JSONType or JSONType[]`),
13
- * with no option to allow it, so the schema would produce no validator; and a
14
- * published `telo.yaml` would stop being JSON Schema, which is what the hub, the
15
- * editor and any third-party reader consume. An `x-` keyword is ignored by
16
- * unaware tooling BY SPECIFICATION, so it degrades to "unconstrained" rather
17
- * than to "cannot validate this kind at all".
18
- *
19
- * ONE RULE, BOTH HALVES. The keyword is implemented as AJV CODEGEN rather than a
20
- * `validate` function so it inlines into standalone-compiled validators — the
21
- * kernel compiles and caches those, and a function-valued keyword would not
22
- * survive serialization. The analyzer registers the identical keyword, and
23
- * `celPlaceholderForSchema` hands a real `Uint8Array` to a CEL leaf at a binary
24
- * slot, so static and runtime agree by construction instead of by two rules kept
25
- * in step. What falls out is the check the annotation exists for: **bytes can
26
- * never be manifest-authored** — no YAML literal is a `Uint8Array` — so a literal
27
- * at a binary slot is rejected while a value arriving by reference passes.
28
- */
29
- export declare const X_TELO_BINARY = "x-telo-binary";
30
- /** True when this schema node declares its value to be raw bytes. */
31
- export declare function isBinarySlot(schema: unknown): boolean;
32
- /**
33
- * The AJV keyword. `Uint8Array` is the structural test rather than any richer
34
- * notion of "binary": it is what every byte producer in the runtime hands over
35
- * (`Buffer` extends it, so Node buffers pass), and it is the one check that means
36
- * the same thing in a browser-side analyzer and in the kernel.
37
- */
38
- export declare function binaryKeyword(): KeywordDefinition;
39
- //# sourceMappingURL=binary-slot.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"binary-slot.d.ts","sourceRoot":"","sources":["../src/binary-slot.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,KAAK,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAMrD;AAWD;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,iBAAiB,CAajD"}
@@ -1,58 +0,0 @@
1
- import * as AjvNS from "ajv";
2
- /**
3
- * `x-telo-binary: true` — the annotation that gives a byte buffer a declared
4
- * identity, and the single accessor every surface reads it through (the
5
- * `ref-slot.ts` precedent).
6
- *
7
- * WHY AN ANNOTATION RATHER THAN A TYPE. Bytes are not expressible in JSON
8
- * Schema's type vocabulary. `type: object` is the closest fit and it is a lie
9
- * that costs a real check: every arbitrary object satisfies it, so a mistyped
10
- * `content: { foo: bar }` reaches the controller instead of failing statically.
11
- * `type: binary` would fix the honesty and break everything else — AJV refuses
12
- * to compile an unknown type at all (`type must be JSONType or JSONType[]`),
13
- * with no option to allow it, so the schema would produce no validator; and a
14
- * published `telo.yaml` would stop being JSON Schema, which is what the hub, the
15
- * editor and any third-party reader consume. An `x-` keyword is ignored by
16
- * unaware tooling BY SPECIFICATION, so it degrades to "unconstrained" rather
17
- * than to "cannot validate this kind at all".
18
- *
19
- * ONE RULE, BOTH HALVES. The keyword is implemented as AJV CODEGEN rather than a
20
- * `validate` function so it inlines into standalone-compiled validators — the
21
- * kernel compiles and caches those, and a function-valued keyword would not
22
- * survive serialization. The analyzer registers the identical keyword, and
23
- * `celPlaceholderForSchema` hands a real `Uint8Array` to a CEL leaf at a binary
24
- * slot, so static and runtime agree by construction instead of by two rules kept
25
- * in step. What falls out is the check the annotation exists for: **bytes can
26
- * never be manifest-authored** — no YAML literal is a `Uint8Array` — so a literal
27
- * at a binary slot is rejected while a value arriving by reference passes.
28
- */
29
- export const X_TELO_BINARY = "x-telo-binary";
30
- /** True when this schema node declares its value to be raw bytes. */
31
- export function isBinarySlot(schema) {
32
- return (typeof schema === "object" &&
33
- schema !== null &&
34
- schema[X_TELO_BINARY] === true);
35
- }
36
- const ajvExports = AjvNS.default ?? AjvNS;
37
- const codegen = ajvExports._ ?? AjvNS._;
38
- /**
39
- * The AJV keyword. `Uint8Array` is the structural test rather than any richer
40
- * notion of "binary": it is what every byte producer in the runtime hands over
41
- * (`Buffer` extends it, so Node buffers pass), and it is the one check that means
42
- * the same thing in a browser-side analyzer and in the kernel.
43
- */
44
- export function binaryKeyword() {
45
- return {
46
- keyword: X_TELO_BINARY,
47
- schemaType: "boolean",
48
- code(cxt) {
49
- // `x-telo-binary: false` states nothing, so it constrains nothing.
50
- if (cxt.schema !== true)
51
- return;
52
- cxt.pass(codegen `${cxt.data} instanceof Uint8Array`);
53
- },
54
- error: {
55
- message: "must be raw bytes (a Uint8Array) — bytes cannot be written inline in a manifest",
56
- },
57
- };
58
- }
@@ -1,71 +0,0 @@
1
- import * as AjvNS from "ajv";
2
- import type { KeywordDefinition } from "ajv";
3
-
4
- /**
5
- * `x-telo-binary: true` — the annotation that gives a byte buffer a declared
6
- * identity, and the single accessor every surface reads it through (the
7
- * `ref-slot.ts` precedent).
8
- *
9
- * WHY AN ANNOTATION RATHER THAN A TYPE. Bytes are not expressible in JSON
10
- * Schema's type vocabulary. `type: object` is the closest fit and it is a lie
11
- * that costs a real check: every arbitrary object satisfies it, so a mistyped
12
- * `content: { foo: bar }` reaches the controller instead of failing statically.
13
- * `type: binary` would fix the honesty and break everything else — AJV refuses
14
- * to compile an unknown type at all (`type must be JSONType or JSONType[]`),
15
- * with no option to allow it, so the schema would produce no validator; and a
16
- * published `telo.yaml` would stop being JSON Schema, which is what the hub, the
17
- * editor and any third-party reader consume. An `x-` keyword is ignored by
18
- * unaware tooling BY SPECIFICATION, so it degrades to "unconstrained" rather
19
- * than to "cannot validate this kind at all".
20
- *
21
- * ONE RULE, BOTH HALVES. The keyword is implemented as AJV CODEGEN rather than a
22
- * `validate` function so it inlines into standalone-compiled validators — the
23
- * kernel compiles and caches those, and a function-valued keyword would not
24
- * survive serialization. The analyzer registers the identical keyword, and
25
- * `celPlaceholderForSchema` hands a real `Uint8Array` to a CEL leaf at a binary
26
- * slot, so static and runtime agree by construction instead of by two rules kept
27
- * in step. What falls out is the check the annotation exists for: **bytes can
28
- * never be manifest-authored** — no YAML literal is a `Uint8Array` — so a literal
29
- * at a binary slot is rejected while a value arriving by reference passes.
30
- */
31
-
32
- export const X_TELO_BINARY = "x-telo-binary";
33
-
34
- /** True when this schema node declares its value to be raw bytes. */
35
- export function isBinarySlot(schema: unknown): boolean {
36
- return (
37
- typeof schema === "object" &&
38
- schema !== null &&
39
- (schema as Record<string, unknown>)[X_TELO_BINARY] === true
40
- );
41
- }
42
-
43
- // AJV's codegen template tag. The package is consumed in both ESM and CJS interop
44
- // shapes, so the named export may sit on the namespace or behind `.default` —
45
- // the same fallback `schema-compat` uses to reach the constructor. Skipping it
46
- // would leave `codegen` undefined under a loader that does not detect AJV's named
47
- // CJS re-exports, and the keyword would throw on its first compile.
48
- type CodegenTag = (s: TemplateStringsArray, ...a: unknown[]) => unknown;
49
- const ajvExports = (AjvNS as any).default ?? AjvNS;
50
- const codegen: CodegenTag = ajvExports._ ?? (AjvNS as any)._;
51
-
52
- /**
53
- * The AJV keyword. `Uint8Array` is the structural test rather than any richer
54
- * notion of "binary": it is what every byte producer in the runtime hands over
55
- * (`Buffer` extends it, so Node buffers pass), and it is the one check that means
56
- * the same thing in a browser-side analyzer and in the kernel.
57
- */
58
- export function binaryKeyword(): KeywordDefinition {
59
- return {
60
- keyword: X_TELO_BINARY,
61
- schemaType: "boolean",
62
- code(cxt: any) {
63
- // `x-telo-binary: false` states nothing, so it constrains nothing.
64
- if (cxt.schema !== true) return;
65
- cxt.pass(codegen`${cxt.data} instanceof Uint8Array`);
66
- },
67
- error: {
68
- message: "must be raw bytes (a Uint8Array) — bytes cannot be written inline in a manifest",
69
- },
70
- } as KeywordDefinition;
71
- }