@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,115 @@
1
+ /** Manifest migrations — the entry vocabulary.
2
+ *
3
+ * A migration is a MATCHER plus a PATCH. The two halves are at different
4
+ * maturities and that is deliberate (`plans/manifest-migrations.md`):
5
+ *
6
+ * - The **patch** names what it targets. Every operation has a known YAML edit
7
+ * form, which is what makes a migration applicable to a *file* at all and
8
+ * what lets the driver derive whether a quick fix exists — read straight off
9
+ * the verb, never declared by the author.
10
+ * - The **matcher** selects every occurrence of a legacy spelling, which is
11
+ * the half a plain patch format has none of (see `match.ts`).
12
+ *
13
+ * BOTH halves are data, and an entry contains no code at all. That is what
14
+ * lets one entry file be read by every kernel: a predicate expressed in one
15
+ * language would mean one artifact is read two ways, invisibly, since a
16
+ * migration that succeeds is silent. A migration that does not fit is a signal
17
+ * to extend the vocabulary, never to hand-write a rewrite. */
18
+ import type { DiagnosticSeverity } from "../types.js";
19
+ import type { MigrationMatch } from "./match.js";
20
+ /** A location inside one manifest document. Segments are mapping keys
21
+ * (strings) and sequence indices (numbers) — the same shape `Document.getIn`
22
+ * takes, so the tree applier and the YAML applier address a node identically. */
23
+ export type MigrationPath = ReadonlyArray<string | number>;
24
+ /** Rename the matched mapping entry's KEY, within its own mapping. Deliberately
25
+ * not JSON Patch's `move`, which relocates a value anywhere in a document
26
+ * across parents and replaces an occupied destination. This refuses an
27
+ * occupied destination instead — silently discarding a value the author wrote
28
+ * is exactly what the leave-it-alone invariant exists to prevent. */
29
+ export interface RenameKeyOperation {
30
+ readonly op: "rename-key";
31
+ readonly to: string;
32
+ }
33
+ /** Replace the value at the matched location.
34
+ *
35
+ * Exactly one of `value` / `qualify` is supplied. `qualify` prefixes the
36
+ * existing string — the shape a spelling rewrite that alias-qualifies a bare
37
+ * name needs, which a literal `value` cannot express because a patch is static
38
+ * data and cannot read the match. */
39
+ export interface SetValueOperation {
40
+ readonly op: "set-value";
41
+ readonly value?: unknown;
42
+ readonly qualify?: string;
43
+ }
44
+ /** Put the matched scalar behind a templating tag (`!cel`, `!ref`, …). `tag` is
45
+ * the engine name without its `!`. */
46
+ export interface SetTagOperation {
47
+ readonly op: "set-tag";
48
+ readonly tag: string;
49
+ }
50
+ /** Insert an item into the matched sequence. `at` defaults to the end. */
51
+ export interface InsertItemOperation {
52
+ readonly op: "insert-item";
53
+ readonly value: unknown;
54
+ readonly at?: number;
55
+ }
56
+ /** Remove the matched mapping entry or sequence item. */
57
+ export interface RemoveEntryOperation {
58
+ readonly op: "remove-entry";
59
+ }
60
+ /** The closed operation vocabulary. Named for what each TARGETS, so the target
61
+ * is never inferred from which parameter happens to be present and the
62
+ * quick-fix question reads off the name. */
63
+ export type MigrationOperation = RenameKeyOperation | SetValueOperation | SetTagOperation | InsertItemOperation | RemoveEntryOperation;
64
+ export declare const MIGRATION_OPS: readonly ["rename-key", "set-value", "set-tag", "insert-item", "remove-entry"];
65
+ /** One legacy spelling and the edit that replaces it. */
66
+ export interface MigrationRule {
67
+ /** Which nodes this rule rewrites, resolved against the frozen pre-migration
68
+ * tree. Declarative — see `match.ts`. */
69
+ readonly match: MigrationMatch;
70
+ /** Applied in order at each match. A patch that cannot apply in full leaves
71
+ * the node untouched. */
72
+ readonly patch: readonly MigrationOperation[];
73
+ }
74
+ /** One deprecation story. May carry several rules — the value-type unification
75
+ * changed three spellings but tells the author one thing, which is why the
76
+ * rationale is entry-level and a mechanical description is not. */
77
+ export interface MigrationEntry {
78
+ /** Stable identifier. Names which migration fired; docs list them. */
79
+ readonly id: string;
80
+ /** Diagnostic code reported for every rewrite this entry makes. */
81
+ readonly code: string;
82
+ readonly severity: DiagnosticSeverity;
83
+ /** A sentence or two of rationale — the one part the driver cannot generate,
84
+ * and the part that makes a deprecation actionable rather than mysterious.
85
+ * Never a clause of the generated sentence. */
86
+ readonly reason: string;
87
+ readonly rules: readonly MigrationRule[];
88
+ }
89
+ /** One applied rewrite. Path provenance is part of the driver's contract:
90
+ * diagnostics are remapped through `legacyPath` before position resolution,
91
+ * and `telo migrate` reads its edit target from the same record — the location
92
+ * in the author's file, not the post-rewrite path. */
93
+ export interface MigrationRewrite {
94
+ readonly entryId: string;
95
+ readonly code: string;
96
+ readonly severity: DiagnosticSeverity;
97
+ readonly documentIndex: number;
98
+ /** Dotted path (`a.b[0].c`) as the AUTHOR wrote it — the key into a
99
+ * position index built from the raw file. */
100
+ readonly legacyPath: string;
101
+ /** Dotted path after the rewrite. Equal to `legacyPath` unless a
102
+ * `rename-key` moved it. */
103
+ readonly migratedPath: string;
104
+ /** Human-readable description of what changed, generated by the driver. */
105
+ readonly summary: string;
106
+ }
107
+ /** Why a matched patch was refused. A migration that cannot rewrite leaves the
108
+ * node untouched for the ordinary validator to reject — never guessing, never
109
+ * dropping. */
110
+ export type MigrationRefusal = "path-not-found" | "destination-occupied" | "not-a-mapping-entry" | "not-a-sequence" | "not-a-scalar" | "malformed-value"
111
+ /** The value is already what the patch would write. A rule should match only
112
+ * the legacy spelling, so this means its matcher was too wide — refusing
113
+ * keeps that from surfacing as a deprecation the author cannot act on. */
114
+ | "nothing-to-rewrite";
115
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/migrations/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;+DAgB+D;AAE/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;kFAEkF;AAClF,MAAM,MAAM,aAAa,GAAG,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;AAE3D;;;;sEAIsE;AACtE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED;;;;;sCAKsC;AACtC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;uCACuC;AACvC,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,aAAa,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAC;CAC7B;AAED;;6CAE6C;AAC7C,MAAM,MAAM,kBAAkB,GAC1B,kBAAkB,GAClB,iBAAiB,GACjB,eAAe,GACf,mBAAmB,GACnB,oBAAoB,CAAC;AAEzB,eAAO,MAAM,aAAa,gFAMhB,CAAC;AAEX,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B;8CAC0C;IAC1C,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B;8BAC0B;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAC/C;AAED;;oEAEoE;AACpE,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC;;oDAEgD;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,CAAC;CAC1C;AAED;;;uDAGuD;AACvD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;kDAC8C;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;iCAC6B;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;gBAEgB;AAChB,MAAM,MAAM,gBAAgB,GACxB,gBAAgB,GAChB,sBAAsB,GACtB,qBAAqB,GACrB,gBAAgB,GAChB,cAAc,GACd,iBAAiB;AACnB;;2EAE2E;GACzE,oBAAoB,CAAC"}
@@ -0,0 +1,24 @@
1
+ /** Manifest migrations — the entry vocabulary.
2
+ *
3
+ * A migration is a MATCHER plus a PATCH. The two halves are at different
4
+ * maturities and that is deliberate (`plans/manifest-migrations.md`):
5
+ *
6
+ * - The **patch** names what it targets. Every operation has a known YAML edit
7
+ * form, which is what makes a migration applicable to a *file* at all and
8
+ * what lets the driver derive whether a quick fix exists — read straight off
9
+ * the verb, never declared by the author.
10
+ * - The **matcher** selects every occurrence of a legacy spelling, which is
11
+ * the half a plain patch format has none of (see `match.ts`).
12
+ *
13
+ * BOTH halves are data, and an entry contains no code at all. That is what
14
+ * lets one entry file be read by every kernel: a predicate expressed in one
15
+ * language would mean one artifact is read two ways, invisibly, since a
16
+ * migration that succeeds is silent. A migration that does not fit is a signal
17
+ * to extend the vocabulary, never to hand-write a rewrite. */
18
+ export const MIGRATION_OPS = [
19
+ "rename-key",
20
+ "set-value",
21
+ "set-tag",
22
+ "insert-item",
23
+ "remove-entry",
24
+ ];
@@ -0,0 +1,21 @@
1
+ /** Rendering a migration plan back into the author's YAML.
2
+ *
3
+ * Edits are BYTE SPLICES computed from the parsed document's node ranges, not
4
+ * `Document.toString()`. That is `cli/nodejs/src/commands/upgrade.ts`'s
5
+ * precedent and it is load-bearing here for the same reason: re-serializing
6
+ * re-folds block scalars, drops quote-style hints and reflows long strings, so
7
+ * a one-key repair would arrive as a whole-file diff. Everything outside the
8
+ * spliced ranges is byte-identical to the input.
9
+ *
10
+ * The document is parsed with the same `defaultCustomTags()` every other
11
+ * parse site uses, so a tagged scalar is a node here exactly as it is in the
12
+ * loader's tree. */
13
+ import { type Document } from "yaml";
14
+ import { type TextEdit } from "../yaml-source-edit.js";
15
+ import type { MigrationEffect } from "./patch.js";
16
+ export { applyTextEdits, type TextEdit } from "../yaml-source-edit.js";
17
+ /** One YAML edit per effect, or `undefined` when the effect cannot be written
18
+ * into this file — the same all-or-nothing rule the tree side has, so a
19
+ * partially applied patch never reaches disk. */
20
+ export declare function planTextEdits(doc: Document, text: string, effects: readonly MigrationEffect[]): TextEdit[] | undefined;
21
+ //# sourceMappingURL=yaml-edit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yaml-edit.d.ts","sourceRoot":"","sources":["../../src/migrations/yaml-edit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;qBAWqB;AAErB,OAAO,EAAkC,KAAK,QAAQ,EAAa,MAAM,MAAM,CAAC;AAChF,OAAO,EAAwB,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlD,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AA2CvE;;kDAEkD;AAClD,wBAAgB,aAAa,CAC3B,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,eAAe,EAAE,GAClC,QAAQ,EAAE,GAAG,SAAS,CAkFxB"}
@@ -0,0 +1,245 @@
1
+ /** Rendering a migration plan back into the author's YAML.
2
+ *
3
+ * Edits are BYTE SPLICES computed from the parsed document's node ranges, not
4
+ * `Document.toString()`. That is `cli/nodejs/src/commands/upgrade.ts`'s
5
+ * precedent and it is load-bearing here for the same reason: re-serializing
6
+ * re-folds block scalars, drops quote-style hints and reflows long strings, so
7
+ * a one-key repair would arrive as a whole-file diff. Everything outside the
8
+ * spliced ranges is byte-identical to the input.
9
+ *
10
+ * The document is parsed with the same `defaultCustomTags()` every other
11
+ * parse site uses, so a tagged scalar is a node here exactly as it is in the
12
+ * loader's tree. */
13
+ import { isMap, isPair, isScalar, isSeq } from "yaml";
14
+ import { renderFixReplacement } from "../yaml-source-edit.js";
15
+ export { applyTextEdits } from "../yaml-source-edit.js";
16
+ function nodeAt(doc, path) {
17
+ if (path.length === 0)
18
+ return doc.contents;
19
+ return doc.getIn(path, true);
20
+ }
21
+ function rangeOf(node) {
22
+ const range = node?.range;
23
+ if (!Array.isArray(range) || typeof range[0] !== "number" || typeof range[1] !== "number") {
24
+ return undefined;
25
+ }
26
+ return [range[0], range[1]];
27
+ }
28
+ /** The Pair whose key is `key` inside the map at `parent`. */
29
+ function pairAt(doc, parent, key) {
30
+ const map = nodeAt(doc, parent);
31
+ if (!isMap(map))
32
+ return undefined;
33
+ return map.items.find((item) => isPair(item) && isScalar(item.key) && String(item.key.value) === key);
34
+ }
35
+ /** Re-quote `value` in the style the author used at `original`.
36
+ *
37
+ * A non-string is rendered as its YAML spelling and then handed to the shared
38
+ * rule, so a migration and a quick fix quote the same value identically — the
39
+ * rule itself is `renderFixReplacement` (`../yaml-source-edit.js`), which both
40
+ * this and `@telorun/ide-support` read rather than restate. */
41
+ function renderScalar(original, value) {
42
+ if (value === null)
43
+ return "null";
44
+ if (typeof value === "boolean" || typeof value === "number")
45
+ return String(value);
46
+ if (typeof value !== "string")
47
+ return undefined;
48
+ return renderFixReplacement(original, value);
49
+ }
50
+ /** Column of `offset` within its line, and the offset of that line's start. */
51
+ function lineGeometry(text, offset) {
52
+ const lineStart = text.lastIndexOf("\n", Math.max(0, offset - 1)) + 1;
53
+ return { lineStart, indent: offset - lineStart };
54
+ }
55
+ /** One YAML edit per effect, or `undefined` when the effect cannot be written
56
+ * into this file — the same all-or-nothing rule the tree side has, so a
57
+ * partially applied patch never reaches disk. */
58
+ export function planTextEdits(doc, text, effects) {
59
+ if (effects.length === 0)
60
+ return [];
61
+ // **The document is never mutated.** A patch's later effects address the node
62
+ // by its POST-rename path, which the file does not have — so every effect is
63
+ // resolved at the one location the patch matched, read off the first effect.
64
+ // The tree applier and this one therefore agree without either replaying the
65
+ // other's state.
66
+ const first = effects[0];
67
+ const docPath = first.kind === "rename-key" ? [...first.parent, first.from] : first.path;
68
+ // A removal supersedes every other edit at the location: `planPatch` refuses
69
+ // anything after a `remove-entry`, and the line it deletes contains them.
70
+ const removal = effects.find((e) => e.kind === "remove-entry");
71
+ if (removal)
72
+ return removeEntryEdit(doc, text, docPath);
73
+ const edits = [];
74
+ let keyEdit;
75
+ // Value edits compose into ONE splice: a `set-tag` after a `set-value` must
76
+ // emit the tag in front of the NEW scalar, not re-read the old one.
77
+ let valueSpan;
78
+ let scalarText;
79
+ let tagText;
80
+ for (const effect of effects) {
81
+ switch (effect.kind) {
82
+ case "rename-key": {
83
+ const pair = pairAt(doc, docPath.slice(0, -1), String(docPath[docPath.length - 1]));
84
+ if (!isPair(pair) || !isScalar(pair.key))
85
+ return undefined;
86
+ const range = rangeOf(pair.key);
87
+ if (!range)
88
+ return undefined;
89
+ const rendered = renderScalar(text.slice(range[0], range[1]), effect.to);
90
+ if (rendered === undefined)
91
+ return undefined;
92
+ keyEdit = { start: range[0], end: range[1], newText: rendered };
93
+ break;
94
+ }
95
+ case "set-value": {
96
+ const range = rangeOf(nodeAt(doc, docPath));
97
+ if (!range)
98
+ return undefined;
99
+ const rendered = renderScalar(text.slice(range[0], range[1]), effect.value);
100
+ if (rendered === undefined)
101
+ return undefined;
102
+ valueSpan ??= range;
103
+ scalarText = rendered;
104
+ break;
105
+ }
106
+ case "set-tag": {
107
+ const node = nodeAt(doc, docPath);
108
+ if (!isScalar(node))
109
+ return undefined;
110
+ const range = rangeOf(node);
111
+ if (!range)
112
+ return undefined;
113
+ // The tag sits outside the scalar's own span, so an existing one is
114
+ // replaced by rewriting from where it starts; a plain scalar just gains
115
+ // a prefix.
116
+ const existing = typeof node.tag === "string" ? node.tag : undefined;
117
+ const tagStart = existing ? text.lastIndexOf(existing, range[0]) : -1;
118
+ valueSpan = [tagStart >= 0 ? tagStart : range[0], range[1]];
119
+ scalarText ??= text.slice(range[0], range[1]);
120
+ tagText = `!${effect.tag}`;
121
+ break;
122
+ }
123
+ case "insert-item": {
124
+ const edit = insertItemEdit(doc, text, docPath, effect.index, effect.value);
125
+ if (!edit)
126
+ return undefined;
127
+ edits.push(edit);
128
+ break;
129
+ }
130
+ case "remove-entry":
131
+ // Handled above.
132
+ break;
133
+ }
134
+ }
135
+ if (keyEdit)
136
+ edits.push(keyEdit);
137
+ if (valueSpan && scalarText !== undefined) {
138
+ edits.push({
139
+ start: valueSpan[0],
140
+ end: valueSpan[1],
141
+ newText: tagText ? `${tagText} ${scalarText}` : scalarText,
142
+ });
143
+ }
144
+ return edits;
145
+ }
146
+ function insertItemEdit(doc, text, path, index, value) {
147
+ const seq = nodeAt(doc, path);
148
+ // An empty sequence is written `[]` in flow style, which has no item line to
149
+ // extend — the one insert shape this cannot render.
150
+ if (!isSeq(seq) || seq.items.length === 0)
151
+ return undefined;
152
+ const rendered = renderScalar("", value);
153
+ if (rendered === undefined)
154
+ return undefined;
155
+ const anchorIndex = Math.min(index, seq.items.length - 1);
156
+ const anchor = rangeOf(seq.items[anchorIndex]);
157
+ if (!anchor)
158
+ return undefined;
159
+ const { lineStart, indent } = lineGeometry(text, anchor[0]);
160
+ // A block sequence's item starts two columns after its `- `; a flow sequence
161
+ // has no line of its own.
162
+ if (text.slice(lineStart, anchor[0]).trimStart() !== "- ")
163
+ return undefined;
164
+ const line = `${" ".repeat(Math.max(0, indent - 2))}- ${rendered}\n`;
165
+ if (index >= seq.items.length) {
166
+ const last = rangeOf(seq.items[seq.items.length - 1]);
167
+ if (!last)
168
+ return undefined;
169
+ const lineEnd = text.indexOf("\n", last[1]);
170
+ const at = lineEnd < 0 ? text.length : lineEnd + 1;
171
+ return { start: at, end: at, newText: line };
172
+ }
173
+ return { start: lineStart, end: lineStart, newText: line };
174
+ }
175
+ function removeEntryEdit(doc, text, path) {
176
+ const located = locateEntry(doc, path);
177
+ if (!located)
178
+ return undefined;
179
+ const { lineStart } = lineGeometry(text, located.start);
180
+ const prefix = text.slice(lineStart, located.start);
181
+ const opensSequenceItem = /^-\s+$/.test(prefix.trimStart());
182
+ // The entry OWNS its line — indentation for a mapping entry, indentation plus
183
+ // `- ` for a sequence item — so removing the line is exact.
184
+ if (located.item ? opensSequenceItem : prefix.trim() === "") {
185
+ const lineEnd = text.indexOf("\n", located.end);
186
+ return [{ start: lineStart, end: lineEnd < 0 ? text.length : lineEnd + 1, newText: "" }];
187
+ }
188
+ // A mapping entry that OPENS a sequence item shares its line with the `- `,
189
+ // and is the overwhelmingly common shape of the one thing a migration removes
190
+ // today: a legacy ref slot is almost always an `anyOf` branch, written
191
+ // `- type: string` with the annotation beneath it. Deleting the line would
192
+ // take the dash with it and fold the item into its predecessor — so the entry
193
+ // is spliced out up to the FOLLOWING sibling's key instead, which slides onto
194
+ // the dash at the column it already occupies. Refusing here would have made
195
+ // the diagnostic's own advice ("run `telo migrate`") dead for the case it is
196
+ // most often given in.
197
+ if (!located.item && opensSequenceItem) {
198
+ const next = nextSiblingKeyStart(doc, path);
199
+ // Nothing to promote onto the dash: the entry is the item's only one, and
200
+ // removing it would leave `- ` with no value.
201
+ if (next === undefined)
202
+ return undefined;
203
+ // Only whitespace may be swallowed. A comment or anything else between the
204
+ // two entries would be destroyed by the splice, so that stays a hand edit.
205
+ const between = text.slice(located.end, next);
206
+ if (!/^\s*$/.test(between) || !between.includes("\n"))
207
+ return undefined;
208
+ return [{ start: located.start, end: next, newText: "" }];
209
+ }
210
+ return undefined;
211
+ }
212
+ /** Offset of the key of the entry FOLLOWING `path` in its own mapping, or
213
+ * `undefined` when `path` is the last entry (or anything is not a plain
214
+ * key-scalar pair). */
215
+ function nextSiblingKeyStart(doc, path) {
216
+ const map = nodeAt(doc, path.slice(0, -1));
217
+ if (!isMap(map))
218
+ return undefined;
219
+ const key = String(path[path.length - 1]);
220
+ const at = map.items.findIndex((item) => isPair(item) && isScalar(item.key) && String(item.key.value) === key);
221
+ if (at < 0)
222
+ return undefined;
223
+ const next = map.items[at + 1];
224
+ if (!isPair(next) || !isScalar(next.key))
225
+ return undefined;
226
+ return rangeOf(next.key)?.[0];
227
+ }
228
+ /** Span of a whole mapping entry (key through value) or sequence item. */
229
+ function locateEntry(doc, path) {
230
+ const last = path[path.length - 1];
231
+ if (typeof last === "string") {
232
+ const pair = pairAt(doc, path.slice(0, -1), last);
233
+ if (!isPair(pair) || !isScalar(pair.key))
234
+ return undefined;
235
+ const keyRange = rangeOf(pair.key);
236
+ const valueRange = rangeOf(pair.value) ?? keyRange;
237
+ if (!keyRange || !valueRange)
238
+ return undefined;
239
+ return { start: keyRange[0], end: valueRange[1], item: false };
240
+ }
241
+ const range = rangeOf(nodeAt(doc, path));
242
+ if (!range)
243
+ return undefined;
244
+ return { start: range[0], end: range[1], item: true };
245
+ }
@@ -1,11 +1,19 @@
1
1
  import type { Environment } from "@marcbachmann/cel-js";
2
2
  import type { LoadedFile } from "./loaded-types.js";
3
+ import type { MigrationEntry } from "./migrations/types.js";
3
4
  export interface ParseOptions {
4
5
  /** When true, runs `precompileDoc` per document and stamps compiled CEL
5
6
  * on the manifests — same flag `LoadOptions.compile` carries today. */
6
7
  compile?: boolean;
7
8
  /** CEL environment for precompile. Defaults to `buildCelEnvironment()`. */
8
9
  celEnv?: Environment;
10
+ /** When true, the migration phase runs over the parsed documents — legacy
11
+ * spellings rewritten to the current ones before anything else reads the
12
+ * tree. Off by default so a round-trip consumer (the editor) keeps the
13
+ * author's vocabulary; see `LoadOptions.migrate`. */
14
+ migrate?: boolean;
15
+ /** Migration set. Defaults to the analyzer's own `CORE_MIGRATIONS`. */
16
+ migrations?: readonly MigrationEntry[];
9
17
  }
10
18
  /** Pure: text in, structured load result out. No I/O, no caches. */
11
19
  export declare function parseLoadedFile(source: string, requestedUrl: string, text: string, options?: ParseOptions): LoadedFile;
@@ -1 +1 @@
1
- {"version":3,"file":"parse-loaded-file.d.ts","sourceRoot":"","sources":["../src/parse-loaded-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKxD,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,mBAAmB,CAAC;AAKhE,MAAM,WAAW,YAAY;IAC3B;4EACwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA4BD,oEAAoE;AACpE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,YAAY,GACrB,UAAU,CAiDZ"}
1
+ {"version":3,"file":"parse-loaded-file.d.ts","sourceRoot":"","sources":["../src/parse-loaded-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKxD,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,mBAAmB,CAAC;AAEhE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAK5D,MAAM,WAAW,YAAY;IAC3B;4EACwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;0DAGsD;IACtD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uEAAuE;IACvE,UAAU,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CACxC;AA4BD,oEAAoE;AACpE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,YAAY,GACrB,UAAU,CA2DZ"}
@@ -1,6 +1,7 @@
1
1
  import { defaultCustomTags } from "@telorun/templating";
2
2
  import { parseAllDocuments } from "yaml";
3
3
  import { buildCelEnvironment } from "./cel-environment.js";
4
+ import { migrateManifests, NO_MIGRATIONS } from "./migrations/driver.js";
4
5
  import { buildDocumentPositions } from "./position-metadata.js";
5
6
  import { precompileDoc } from "./precompile.js";
6
7
  import { documentToAst } from "./yaml-ast.js";
@@ -42,27 +43,34 @@ export function parseLoadedFile(source, requestedUrl, text, options) {
42
43
  });
43
44
  }
44
45
  });
45
- const manifests = [];
46
- let env;
47
- for (const doc of documents) {
46
+ const manifests = documents.map((doc) => {
48
47
  const raw = doc.toJSON();
49
- if (raw === null || raw === undefined) {
50
- manifests.push(null);
51
- continue;
52
- }
53
- if (options?.compile) {
48
+ return raw === null || raw === undefined ? null : raw;
49
+ });
50
+ // The migration phase, immediately after parse. It runs BEFORE precompile so
51
+ // matchers see the values the author wrote rather than `CompiledValue`
52
+ // wrappers, and — through the loader — before `desugarLoadedFile`, so a rule
53
+ // only ever matches author-written nodes: a synthetic Telo.Import manifest
54
+ // has no YAML document to edit, records a path the file never had, and
55
+ // carries its `variables` / `secrets` by reference from the module doc, so a
56
+ // match inside one would apply twice.
57
+ const migrations = options?.migrate
58
+ ? migrateManifests({ source, manifests, entries: options.migrations })
59
+ : NO_MIGRATIONS;
60
+ let env;
61
+ if (options?.compile) {
62
+ for (let i = 0; i < manifests.length; i++) {
63
+ const raw = manifests[i];
64
+ if (raw === null)
65
+ continue;
54
66
  env ??= options.celEnv ?? buildCelEnvironment();
55
67
  try {
56
- const compiled = precompileDoc(raw, env);
57
- manifests.push(compiled);
68
+ manifests[i] = precompileDoc(raw, env);
58
69
  }
59
70
  catch (error) {
60
71
  throw new Error(`Failed to compile manifest in ${source}: ${error instanceof Error ? error.message : String(error)}`);
61
72
  }
62
73
  }
63
- else {
64
- manifests.push(raw);
65
- }
66
74
  }
67
75
  return {
68
76
  source,
@@ -73,5 +81,6 @@ export function parseLoadedFile(source, requestedUrl, text, options) {
73
81
  manifests,
74
82
  positions,
75
83
  parseErrors,
84
+ migrations,
76
85
  };
77
86
  }
@@ -1,20 +1,37 @@
1
1
  import type { ResourceManifest } from "@telorun/sdk";
2
2
  import type { AliasResolver } from "./alias-resolver.js";
3
3
  /**
4
- * Rewrites import-scoped schema references in place. A `$ref` of the form
5
- * `telo://<authority>/<typeName>` names a `Type.JsonSchema` (or any `Telo.Type`)
6
- * reached through an import: `telo://Self/<type>` for the declaring module's own
7
- * type, `telo://<Alias>/<type>` for an imported module's. Each authority is
8
- * resolved to the owning module's name and the ref is rewritten to the canonical
9
- * `telo://<module>/<type>` the type registered its schema under.
4
+ * Rewrites schema references to their canonical, module-scoped form, in place.
10
5
  *
11
- * The version lives on the `imports:` entry, never the URI only the pinned
12
- * version is loaded, so the canonical id is version-free.
6
+ * A named shape is written with the reference tag `!ref File` for the declaring
7
+ * module's own, `!ref <Alias>.<File>` for one an imported library exports — which
8
+ * is Telo's ONE reference grammar, and `use: schema` has been in the `x-telo-ref`
9
+ * vocabulary for exactly this relation all along (*names a shape; no runtime
10
+ * instance, no edge*). Phase 2.5 has already turned the tag into `{kind, name,
11
+ * alias?}`; this pass turns that into `{ $ref: "telo:<module>/<type>" }`, the id
12
+ * the type registered its schema under.
13
13
  *
14
- * Already-canonical refs (authority is a real module name, not an alias) and
15
- * fragment-bearing built-ins (`telo://manifest#/$defs/ResourceRef`) are left
16
- * untouched: the former because the authority resolves to nothing, the latter
17
- * because they don't match the `authority/type` grammar.
14
+ * WHY NORMALIZE RATHER THAN RESOLVE. Substituting the shape itself would inline
15
+ * it, which changes schema identity — the compiled-validator cache is keyed on it
16
+ * and would make a self-recursive shape inexpressible. Keeping a REFERENCE is
17
+ * what every validator wants, and it is not an AJV concern: a Rust validator
18
+ * wants a registered id just as much. So the tag is the authoring surface and the
19
+ * canonical `$ref` is the internal form, exactly as `resolveRefSentinels` and
20
+ * `resolveSchemaRefKinds` already split authoring sugar from canonical form.
21
+ *
22
+ * ALIAS SCOPE IS WHAT THE CANONICAL FORM CARRIES. The authority is resolved in
23
+ * the DECLARING module's scope, so the id names the owning module and two
24
+ * libraries declaring a shape of the same name stay distinct. A downstream
25
+ * resolver reads the module off the id instead of matching a bare name, which is
26
+ * how an alias used to get silently dropped.
27
+ *
28
+ * The legacy authoring spelling `$ref: "telo://<authority>/<type>"` is resolved
29
+ * the same way, since a published artifact carries it. The version lives on the
30
+ * `imports:` entry, never the URI — only the pinned version is loaded, so the
31
+ * canonical id is version-free. Already-canonical refs and fragment-bearing
32
+ * built-ins (`telo://manifest#/$defs/ResourceRef`) are left untouched: the former
33
+ * because the authority resolves to nothing, the latter because they do not match
34
+ * the `authority/type` grammar.
18
35
  */
19
36
  export declare function resolveSchemaTypeRefs(resources: ResourceManifest[], aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver>): void;
20
37
  //# sourceMappingURL=resolve-schema-type-refs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-schema-type-refs.d.ts","sourceRoot":"","sources":["../src/resolve-schema-type-refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKzD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,IAAI,CAyBN"}
1
+ {"version":3,"file":"resolve-schema-type-refs.d.ts","sourceRoot":"","sources":["../src/resolve-schema-type-refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,IAAI,CA6DN"}