@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,236 @@
1
+ /** Planning and applying a migration patch.
2
+ *
3
+ * Planning is separate from applying because a patch is ALL-OR-NOTHING at each
4
+ * match: a rule whose second operation cannot apply must not leave the first
5
+ * one's edit behind. `planPatch` resolves every operation against the tree
6
+ * first and returns either a concrete effect list or one refusal; only then
7
+ * does an applier run. The same plan drives both appliers — the JSON tree the
8
+ * loader hands the analyzer, and the YAML document `telo migrate` writes — so
9
+ * the in-memory rewrite and the on-disk repair cannot disagree about what an
10
+ * operation means. */
11
+ import { makeTaggedSentinel } from "@telorun/templating";
12
+ import { deepEquals } from "./match.js";
13
+ function isPlainObject(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ /** The container a path's last segment indexes into, or `undefined` when any
17
+ * hop is missing. An empty path has no container — the document root is not a
18
+ * mapping entry and no operation targets it. */
19
+ function containerOf(root, path) {
20
+ if (path.length === 0)
21
+ return undefined;
22
+ let current = root;
23
+ for (let i = 0; i < path.length - 1; i++) {
24
+ const segment = path[i];
25
+ if (typeof segment === "number") {
26
+ if (!Array.isArray(current))
27
+ return undefined;
28
+ current = current[segment];
29
+ }
30
+ else {
31
+ if (!isPlainObject(current))
32
+ return undefined;
33
+ current = current[segment];
34
+ }
35
+ }
36
+ return { container: current, key: path[path.length - 1] };
37
+ }
38
+ function readAt(root, path) {
39
+ const located = containerOf(root, path);
40
+ if (!located)
41
+ return { found: false, value: undefined };
42
+ const { container, key } = located;
43
+ if (typeof key === "number") {
44
+ if (!Array.isArray(container) || key < 0 || key >= container.length) {
45
+ return { found: false, value: undefined };
46
+ }
47
+ return { found: true, value: container[key] };
48
+ }
49
+ if (!isPlainObject(container) || !Object.hasOwn(container, key)) {
50
+ return { found: false, value: undefined };
51
+ }
52
+ return { found: true, value: container[key] };
53
+ }
54
+ /** A scalar YAML can carry behind a tag, and the only thing `set-tag` and
55
+ * `qualify` can operate on. */
56
+ function scalarSource(value) {
57
+ if (typeof value === "string")
58
+ return value;
59
+ if (typeof value === "number" || typeof value === "boolean")
60
+ return String(value);
61
+ return undefined;
62
+ }
63
+ /**
64
+ * Resolve `ops` at `path` against `root`, or refuse.
65
+ *
66
+ * Refusing is the design's safety valve: the node is left exactly as the author
67
+ * wrote it and the ordinary validator reports it with an accurate message,
68
+ * rather than the migration guessing or dropping a value.
69
+ */
70
+ export function planPatch(root, path, ops) {
71
+ const initial = readAt(root, path);
72
+ if (!initial.found)
73
+ return { ok: false, refusal: "path-not-found" };
74
+ const effects = [];
75
+ let currentPath = path;
76
+ let currentValue = initial.value;
77
+ let removed = false;
78
+ for (const op of ops) {
79
+ if (removed)
80
+ return { ok: false, refusal: "path-not-found" };
81
+ switch (op.op) {
82
+ case "rename-key": {
83
+ const located = containerOf(root, currentPath);
84
+ const key = located?.key;
85
+ if (!located || typeof key !== "string" || !isPlainObject(located.container)) {
86
+ return { ok: false, refusal: "not-a-mapping-entry" };
87
+ }
88
+ if (op.to !== key && Object.hasOwn(located.container, op.to)) {
89
+ // Refuse rather than replace: the occupied destination holds a value
90
+ // the author wrote.
91
+ return { ok: false, refusal: "destination-occupied" };
92
+ }
93
+ const parent = currentPath.slice(0, -1);
94
+ effects.push({ kind: "rename-key", parent, from: key, to: op.to });
95
+ currentPath = [...parent, op.to];
96
+ break;
97
+ }
98
+ case "set-value": {
99
+ let next;
100
+ if (op.qualify !== undefined) {
101
+ if (typeof currentValue !== "string") {
102
+ return { ok: false, refusal: "malformed-value" };
103
+ }
104
+ if (currentValue.startsWith(op.qualify)) {
105
+ return { ok: false, refusal: "nothing-to-rewrite" };
106
+ }
107
+ next = `${op.qualify}${currentValue}`;
108
+ }
109
+ else {
110
+ // Same refusal as `qualify`'s, and for the same reason: a rule should
111
+ // match only the legacy spelling, so a write of the value already
112
+ // there means the matcher was too wide. Reporting it would be a
113
+ // deprecation reading `type: string is now written type: string`.
114
+ if (deepEquals(op.value, currentValue)) {
115
+ return { ok: false, refusal: "nothing-to-rewrite" };
116
+ }
117
+ next = op.value;
118
+ }
119
+ effects.push({ kind: "set-value", path: currentPath, value: next });
120
+ currentValue = next;
121
+ break;
122
+ }
123
+ case "set-tag": {
124
+ const source = scalarSource(currentValue);
125
+ if (source === undefined)
126
+ return { ok: false, refusal: "not-a-scalar" };
127
+ effects.push({ kind: "set-tag", path: currentPath, tag: op.tag, source });
128
+ currentValue = makeTaggedSentinel(op.tag, source);
129
+ break;
130
+ }
131
+ case "insert-item": {
132
+ if (!Array.isArray(currentValue))
133
+ return { ok: false, refusal: "not-a-sequence" };
134
+ const index = op.at === undefined
135
+ ? currentValue.length
136
+ : Math.max(0, Math.min(op.at, currentValue.length));
137
+ effects.push({ kind: "insert-item", path: currentPath, index, value: op.value });
138
+ currentValue = [
139
+ ...currentValue.slice(0, index),
140
+ op.value,
141
+ ...currentValue.slice(index),
142
+ ];
143
+ break;
144
+ }
145
+ case "remove-entry": {
146
+ effects.push({ kind: "remove-entry", path: currentPath });
147
+ currentValue = undefined;
148
+ removed = true;
149
+ break;
150
+ }
151
+ }
152
+ }
153
+ return {
154
+ ok: true,
155
+ plan: {
156
+ effects,
157
+ finalPath: currentPath,
158
+ before: initial.value,
159
+ after: removed ? undefined : currentValue,
160
+ },
161
+ };
162
+ }
163
+ /** Apply a plan to the in-memory manifest tree. Mutates in place — the tree is
164
+ * the loader's own projection of the document, never the author's file. */
165
+ export function applyEffectsToTree(root, effects) {
166
+ for (const effect of effects) {
167
+ switch (effect.kind) {
168
+ case "rename-key": {
169
+ const parent = effect.parent.length === 0 ? root : readAt(root, effect.parent).value;
170
+ if (!isPlainObject(parent))
171
+ continue;
172
+ // Rebuilt in place so the renamed key keeps its position. Key order is
173
+ // what a round-trip consumer and a rendered diff both read.
174
+ const entries = Object.entries(parent);
175
+ for (const [key] of entries)
176
+ delete parent[key];
177
+ for (const [key, value] of entries) {
178
+ parent[key === effect.from ? effect.to : key] = value;
179
+ }
180
+ break;
181
+ }
182
+ case "set-value":
183
+ case "set-tag": {
184
+ const located = containerOf(root, effect.path);
185
+ if (!located)
186
+ continue;
187
+ const next = effect.kind === "set-value"
188
+ ? effect.value
189
+ : makeTaggedSentinel(effect.tag, effect.source);
190
+ writeAt(located.container, located.key, next);
191
+ break;
192
+ }
193
+ case "insert-item": {
194
+ const target = readAt(root, effect.path).value;
195
+ if (!Array.isArray(target))
196
+ continue;
197
+ target.splice(effect.index, 0, effect.value);
198
+ break;
199
+ }
200
+ case "remove-entry": {
201
+ const located = containerOf(root, effect.path);
202
+ if (!located)
203
+ continue;
204
+ if (typeof located.key === "number") {
205
+ if (Array.isArray(located.container))
206
+ located.container.splice(located.key, 1);
207
+ }
208
+ else if (isPlainObject(located.container)) {
209
+ delete located.container[located.key];
210
+ }
211
+ break;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ function writeAt(container, key, value) {
217
+ if (typeof key === "number") {
218
+ if (Array.isArray(container))
219
+ container[key] = value;
220
+ return;
221
+ }
222
+ if (isPlainObject(container))
223
+ container[key] = value;
224
+ }
225
+ /** Dotted rendering of a path, in the form the position index and every
226
+ * diagnostic's `data.path` use: `routes[0].handler`. */
227
+ export function formatMigrationPath(path) {
228
+ let out = "";
229
+ for (const segment of path) {
230
+ if (typeof segment === "number")
231
+ out += `[${segment}]`;
232
+ else
233
+ out += out.length === 0 ? segment : `.${segment}`;
234
+ }
235
+ return out;
236
+ }
@@ -0,0 +1,25 @@
1
+ /** Path provenance — part of the driver's contract, not an optional extra.
2
+ *
3
+ * `resolveRange` looks a diagnostic's dotted path up in a position index built
4
+ * from the RAW file, then falls back to the parent's key or value range. Every
5
+ * rewrite that existed before migrations preserved paths; a `rename-key` is
6
+ * the first that does not, and without a remap every downstream diagnostic on
7
+ * that node degrades to a parent squiggle — with a `DiagnosticFix` over a
8
+ * single-line parent value writing a whole value across it.
9
+ *
10
+ * So each rewrite records the legacy path it matched alongside the migrated
11
+ * one, and diagnostics are remapped through that record before position
12
+ * resolution. This is the generalization of `rewriteSyntheticOrigins`, which
13
+ * already rewrites `data.path` so lookups resolve after
14
+ * `normalizeInlineResources` has moved a resource. */
15
+ import type { AnalysisDiagnostic } from "../types.js";
16
+ import type { LoadedGraph } from "../loaded-types.js";
17
+ /**
18
+ * Rewrite every diagnostic's `data.path` from the migrated spelling back to
19
+ * what the author wrote, so position lookups against the raw file resolve.
20
+ *
21
+ * A no-op — and returns the input array — when nothing in the graph was
22
+ * migrated, which is the overwhelmingly common case.
23
+ */
24
+ export declare function remapMigratedPaths(graph: LoadedGraph, diagnostics: readonly AnalysisDiagnostic[]): AnalysisDiagnostic[];
25
+ //# sourceMappingURL=provenance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provenance.d.ts","sourceRoot":"","sources":["../../src/migrations/provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;uDAauD;AAEvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAsHtD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,WAAW,EAClB,WAAW,EAAE,SAAS,kBAAkB,EAAE,GACzC,kBAAkB,EAAE,CAsBtB"}
@@ -0,0 +1,118 @@
1
+ /** Path provenance — part of the driver's contract, not an optional extra.
2
+ *
3
+ * `resolveRange` looks a diagnostic's dotted path up in a position index built
4
+ * from the RAW file, then falls back to the parent's key or value range. Every
5
+ * rewrite that existed before migrations preserved paths; a `rename-key` is
6
+ * the first that does not, and without a remap every downstream diagnostic on
7
+ * that node degrades to a parent squiggle — with a `DiagnosticFix` over a
8
+ * single-line parent value writing a whole value across it.
9
+ *
10
+ * So each rewrite records the legacy path it matched alongside the migrated
11
+ * one, and diagnostics are remapped through that record before position
12
+ * resolution. This is the generalization of `rewriteSyntheticOrigins`, which
13
+ * already rewrites `data.path` so lookups resolve after
14
+ * `normalizeInlineResources` has moved a resource. */
15
+ function identityKey(kind, name) {
16
+ return `${kind}\0${name}`;
17
+ }
18
+ function push(index, key, record) {
19
+ const bucket = index.get(key);
20
+ if (bucket)
21
+ bucket.push(record);
22
+ else
23
+ index.set(key, [record]);
24
+ }
25
+ function buildIndex(graph) {
26
+ const byFile = new Map();
27
+ const byIdentity = new Map();
28
+ let size = 0;
29
+ for (const mod of graph.modules.values()) {
30
+ for (const file of [mod.owner, ...mod.partials]) {
31
+ for (const rewrite of file.migrations.rewrites) {
32
+ const manifest = file.manifests[rewrite.documentIndex];
33
+ const kind = typeof manifest?.kind === "string" ? manifest.kind : undefined;
34
+ const name = typeof manifest?.metadata?.name === "string" ? manifest.metadata.name : undefined;
35
+ const record = { source: file.source, kind, name, rewrite };
36
+ push(byFile, file.source, record);
37
+ if (kind !== undefined && name !== undefined) {
38
+ push(byIdentity, identityKey(kind, name), record);
39
+ }
40
+ size++;
41
+ }
42
+ }
43
+ }
44
+ return { byFile, byIdentity, size };
45
+ }
46
+ /** The records a diagnostic may be remapped against, narrowed by whichever
47
+ * routing facts it carries. `undefined` means "cannot be narrowed to one
48
+ * file", the one case where no answer is better than a guess. */
49
+ function candidatesFor(index, filePath, kind, name) {
50
+ if (kind !== undefined && name !== undefined) {
51
+ const records = index.byIdentity.get(identityKey(kind, name));
52
+ if (!records)
53
+ return undefined;
54
+ if (filePath !== undefined)
55
+ return records.filter((r) => r.source === filePath);
56
+ // Several files declare this identity and the diagnostic names none of
57
+ // them. Guessing would move a squiggle onto an unrelated node, which is
58
+ // worse than the parent-squiggle fallback leaving it where it is.
59
+ return new Set(records.map((r) => r.source)).size > 1 ? undefined : records;
60
+ }
61
+ if (filePath !== undefined)
62
+ return index.byFile.get(filePath);
63
+ return undefined;
64
+ }
65
+ /** `path` with the longest matching migrated prefix swapped back to the legacy
66
+ * spelling, or `undefined` when no rewrite touched it. Longest-prefix wins so
67
+ * a rewrite nested inside another resolves against the innermost one.
68
+ *
69
+ * Two records covering the same path with DIFFERENT legacy spellings are
70
+ * ambiguous — a file-scoped candidate set spans every document in the file,
71
+ * and two documents can share a path. Refusing beats picking one. */
72
+ function remapPath(path, records) {
73
+ let best;
74
+ let ambiguous = false;
75
+ for (const { rewrite } of records) {
76
+ const migrated = rewrite.migratedPath;
77
+ if (migrated === rewrite.legacyPath)
78
+ continue;
79
+ const covers = path === migrated || path.startsWith(`${migrated}.`) || path.startsWith(`${migrated}[`);
80
+ if (!covers)
81
+ continue;
82
+ if (!best || migrated.length > best.migratedPath.length) {
83
+ best = rewrite;
84
+ ambiguous = false;
85
+ }
86
+ else if (migrated.length === best.migratedPath.length &&
87
+ rewrite.legacyPath !== best.legacyPath) {
88
+ ambiguous = true;
89
+ }
90
+ }
91
+ if (!best || ambiguous)
92
+ return undefined;
93
+ return best.legacyPath + path.slice(best.migratedPath.length);
94
+ }
95
+ /**
96
+ * Rewrite every diagnostic's `data.path` from the migrated spelling back to
97
+ * what the author wrote, so position lookups against the raw file resolve.
98
+ *
99
+ * A no-op — and returns the input array — when nothing in the graph was
100
+ * migrated, which is the overwhelmingly common case.
101
+ */
102
+ export function remapMigratedPaths(graph, diagnostics) {
103
+ const index = buildIndex(graph);
104
+ if (index.size === 0)
105
+ return [...diagnostics];
106
+ return diagnostics.map((d) => {
107
+ const data = d.data;
108
+ if (typeof data?.path !== "string")
109
+ return d;
110
+ const records = candidatesFor(index, typeof data.filePath === "string" ? data.filePath : undefined, typeof data.resource?.kind === "string" ? data.resource.kind : undefined, typeof data.resource?.name === "string" ? data.resource.name : undefined);
111
+ if (!records || records.length === 0)
112
+ return d;
113
+ const remapped = remapPath(data.path, records);
114
+ if (remapped === undefined || remapped === data.path)
115
+ return d;
116
+ return { ...d, data: { ...data, path: remapped } };
117
+ });
118
+ }
@@ -0,0 +1,35 @@
1
+ /** The core migration set — entries that ship with the analyzer and may match
2
+ * any node.
3
+ *
4
+ * **The set is data, not code.** One JSON file per entry, canonical at
5
+ * `analyzer/migrations/`, read as one lexically ordered list: adding a
6
+ * migration is one file, and retiring one is deleting it. Nothing here lists
7
+ * the entries — TypeScript has no glob import, so the barrel beside the copies
8
+ * is emitted from the same directory listing that produced them. A
9
+ * hand-maintained list was the one place this mechanism could fail silently:
10
+ * add a file, forget the import, and the migration never fires, which looks
11
+ * exactly like one that ran.
12
+ *
13
+ * The files live beside the language implementations rather than inside any
14
+ * one of them because every kernel must apply the IDENTICAL rewrite — a
15
+ * rewrite added to one side would mean one artifact means two things on two
16
+ * kernels, invisibly, since a migration that succeeds is silent. Only
17
+ * `analyzer/nodejs` reads them today — the Rust reader is planned, and until
18
+ * it lands the Rust kernel applies NO migration, so a legacy spelling it
19
+ * cannot otherwise interpret fails there rather than being rewritten. When it
20
+ * lands it will embed these files with `include_str!`; a Go half would use
21
+ * `//go:embed`. JSON rather than YAML for one reason: it is the only format
22
+ * all three embed with no generation step, because TypeScript's only native
23
+ * embed is `resolveJsonModule`. The copy under `./entries/` is made by the
24
+ * analyzer's `prepare` (`scripts/copy-migration-entries.mjs`) — identical
25
+ * bytes, so no entry's MEANING is ever derived from anything.
26
+ *
27
+ * **Entries carry no version stamp.** "Can this be deleted?" turns on whether
28
+ * any published artifact still carries the legacy spelling, which the
29
+ * artifact's own release version cannot answer — the hub can, since it caches
30
+ * every tracked module version's `telo.yaml`. A stamp would record when an
31
+ * entry was written, which git already does, while looking like an answer to a
32
+ * question it does not address. */
33
+ import type { MigrationEntry } from "./types.js";
34
+ export declare const CORE_MIGRATIONS: readonly MigrationEntry[];
35
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/migrations/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oCA+BoC;AAIpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,eAAO,MAAM,eAAe,EAAE,SAAS,cAAc,EAEpD,CAAC"}
@@ -0,0 +1,35 @@
1
+ /** The core migration set — entries that ship with the analyzer and may match
2
+ * any node.
3
+ *
4
+ * **The set is data, not code.** One JSON file per entry, canonical at
5
+ * `analyzer/migrations/`, read as one lexically ordered list: adding a
6
+ * migration is one file, and retiring one is deleting it. Nothing here lists
7
+ * the entries — TypeScript has no glob import, so the barrel beside the copies
8
+ * is emitted from the same directory listing that produced them. A
9
+ * hand-maintained list was the one place this mechanism could fail silently:
10
+ * add a file, forget the import, and the migration never fires, which looks
11
+ * exactly like one that ran.
12
+ *
13
+ * The files live beside the language implementations rather than inside any
14
+ * one of them because every kernel must apply the IDENTICAL rewrite — a
15
+ * rewrite added to one side would mean one artifact means two things on two
16
+ * kernels, invisibly, since a migration that succeeds is silent. Only
17
+ * `analyzer/nodejs` reads them today — the Rust reader is planned, and until
18
+ * it lands the Rust kernel applies NO migration, so a legacy spelling it
19
+ * cannot otherwise interpret fails there rather than being rewritten. When it
20
+ * lands it will embed these files with `include_str!`; a Go half would use
21
+ * `//go:embed`. JSON rather than YAML for one reason: it is the only format
22
+ * all three embed with no generation step, because TypeScript's only native
23
+ * embed is `resolveJsonModule`. The copy under `./entries/` is made by the
24
+ * analyzer's `prepare` (`scripts/copy-migration-entries.mjs`) — identical
25
+ * bytes, so no entry's MEANING is ever derived from anything.
26
+ *
27
+ * **Entries carry no version stamp.** "Can this be deleted?" turns on whether
28
+ * any published artifact still carries the legacy spelling, which the
29
+ * artifact's own release version cannot answer — the hub can, since it caches
30
+ * every tracked module version's `telo.yaml`. A stamp would record when an
31
+ * entry was written, which git already does, while looking like an answer to a
32
+ * question it does not address. */
33
+ import { parseMigrationEntry } from "./entry-data.js";
34
+ import { MIGRATION_ENTRY_FILES } from "./entries/index.js";
35
+ export const CORE_MIGRATIONS = MIGRATION_ENTRY_FILES.map(([file, data]) => parseMigrationEntry(file, data));
@@ -0,0 +1,28 @@
1
+ /** How a rewrite is REPORTED — the provenance record and the diagnostic.
2
+ *
3
+ * Split from the driver because the two answer different questions: the driver
4
+ * owns the guarantees (one pass, a frozen match set, all-or-nothing patches,
5
+ * refusal rather than a guess), while everything here is about telling an
6
+ * author what happened. Nothing in this file can change what a migration does.
7
+ *
8
+ * **A diagnostic composes in three parts and the entry writes only one.** What
9
+ * changed and how to apply it are generated identically for every entry, from
10
+ * the matched key and value, the replacement, and the operation verbs. What
11
+ * the driver cannot know is *why*, which is the part that makes a deprecation
12
+ * actionable rather than mysterious — so an entry supplies `reason`, and never
13
+ * a clause of the generated sentence. */
14
+ import type { ResourceManifest } from "@telorun/sdk";
15
+ import { type PatchPlan } from "./patch.js";
16
+ import type { MigrationEntry, MigrationOperation, MigrationPath, MigrationRewrite } from "./types.js";
17
+ import type { AnalysisDiagnostic } from "../types.js";
18
+ /** One patch that applied, as the reporting side needs to see it. */
19
+ export interface AppliedPatch {
20
+ readonly entry: MigrationEntry;
21
+ readonly documentIndex: number;
22
+ readonly matched: MigrationPath;
23
+ readonly plan: PatchPlan;
24
+ readonly ops: readonly MigrationOperation[];
25
+ }
26
+ export declare function toRewrite(applied: AppliedPatch): MigrationRewrite;
27
+ export declare function toDiagnostic(applied: AppliedPatch, rewrite: MigrationRewrite, source: string, manifests: ReadonlyArray<ResourceManifest | null>): AnalysisDiagnostic;
28
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/migrations/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;0CAY0C;AAE1C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAA6C,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AACvF,OAAO,KAAK,EACV,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EACjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,kBAAkB,EAAiB,MAAM,aAAa,CAAC;AAErE,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAC7C;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,gBAAgB,CAUjE;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,aAAa,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAChD,kBAAkB,CA+BpB"}
@@ -0,0 +1,144 @@
1
+ /** How a rewrite is REPORTED — the provenance record and the diagnostic.
2
+ *
3
+ * Split from the driver because the two answer different questions: the driver
4
+ * owns the guarantees (one pass, a frozen match set, all-or-nothing patches,
5
+ * refusal rather than a guess), while everything here is about telling an
6
+ * author what happened. Nothing in this file can change what a migration does.
7
+ *
8
+ * **A diagnostic composes in three parts and the entry writes only one.** What
9
+ * changed and how to apply it are generated identically for every entry, from
10
+ * the matched key and value, the replacement, and the operation verbs. What
11
+ * the driver cannot know is *why*, which is the part that makes a deprecation
12
+ * actionable rather than mysterious — so an entry supplies `reason`, and never
13
+ * a clause of the generated sentence. */
14
+ import { isTaggedSentinel } from "@telorun/templating";
15
+ import { formatMigrationPath } from "./patch.js";
16
+ export function toRewrite(applied) {
17
+ return {
18
+ entryId: applied.entry.id,
19
+ code: applied.entry.code,
20
+ severity: applied.entry.severity,
21
+ documentIndex: applied.documentIndex,
22
+ legacyPath: formatMigrationPath(applied.matched),
23
+ migratedPath: formatMigrationPath(applied.plan.finalPath),
24
+ summary: describeChange(applied),
25
+ };
26
+ }
27
+ export function toDiagnostic(applied, rewrite, source, manifests) {
28
+ const fix = deriveFix(applied);
29
+ const closing = fix
30
+ ? "Run `telo migrate` to apply it."
31
+ : `no quick fix (${refusalPhrase(applied.ops)}) — run \`telo migrate\``;
32
+ const manifest = manifests[applied.documentIndex];
33
+ const kind = manifest?.kind;
34
+ const name = manifest?.metadata?.name;
35
+ return {
36
+ severity: applied.entry.severity,
37
+ code: applied.entry.code,
38
+ source: "telo-analyzer",
39
+ message: `${rewrite.summary}\n${applied.entry.reason}\n${closing}`,
40
+ data: {
41
+ filePath: source,
42
+ // The AUTHOR's path, not the post-rewrite one: the position index is
43
+ // built from the raw file and knows only the spelling that is in it.
44
+ path: rewrite.legacyPath,
45
+ ...(typeof kind === "string" && typeof name === "string"
46
+ ? { resource: { kind, name } }
47
+ : {}),
48
+ ...(fix ? { fix } : {}),
49
+ migration: {
50
+ id: applied.entry.id,
51
+ legacyPath: rewrite.legacyPath,
52
+ migratedPath: rewrite.migratedPath,
53
+ },
54
+ },
55
+ };
56
+ }
57
+ /**
58
+ * Whether a quick fix exists is DERIVED from the operations, never declared.
59
+ *
60
+ * `DiagnosticFix` is a whole-value `replacement` written over a value node's
61
+ * span, and it promises a repair applicable without review. A lone `set-value`
62
+ * producing a scalar is exactly that. Anything else — a key rename, a tag, a
63
+ * collection edit, a structured value — has no honest whole-value form, and the
64
+ * diagnostic says so rather than offering a repair that would corrupt the file.
65
+ * The derivation is total, so a migration never silently lacks one.
66
+ */
67
+ function deriveFix(applied) {
68
+ if (applied.ops.length !== 1)
69
+ return undefined;
70
+ const only = applied.ops[0];
71
+ if (only.op !== "set-value")
72
+ return undefined;
73
+ const value = applied.plan.after;
74
+ if (typeof value === "string")
75
+ return { replacement: value };
76
+ if (typeof value === "number" || typeof value === "boolean" || value === null) {
77
+ return { replacement: String(value) };
78
+ }
79
+ return undefined;
80
+ }
81
+ function refusalPhrase(ops) {
82
+ for (const op of ops) {
83
+ switch (op.op) {
84
+ case "rename-key":
85
+ return "renames a key";
86
+ case "set-tag":
87
+ return "adds a tag";
88
+ case "insert-item":
89
+ return "inserts an item";
90
+ case "remove-entry":
91
+ return "removes an entry";
92
+ default:
93
+ break;
94
+ }
95
+ }
96
+ return "writes a structured value";
97
+ }
98
+ /** Only the items an `insert-item` added, narrowed by the effect's own kind so
99
+ * the value is read off a typed field rather than cast out of a union. */
100
+ function insertedValues(effects) {
101
+ const values = [];
102
+ for (const effect of effects) {
103
+ if (effect.kind !== "insert-item")
104
+ return undefined;
105
+ values.push(effect.value);
106
+ }
107
+ return values.length > 0 ? values : undefined;
108
+ }
109
+ /** The generated "what changed" sentence — the half no entry writes. */
110
+ function describeChange(applied) {
111
+ const legacyKey = lastSegment(applied.matched);
112
+ const migratedKey = lastSegment(applied.plan.finalPath);
113
+ const { before, after, effects } = applied.plan;
114
+ if (after === undefined && effects.some((e) => e.kind === "remove-entry")) {
115
+ return `\`${legacyKey}\` is no longer used.`;
116
+ }
117
+ const inserted = insertedValues(effects);
118
+ if (inserted) {
119
+ return `\`${formatMigrationPath(applied.matched)}\` gains \`${inserted
120
+ .map(renderValue)
121
+ .join("`, `")}\`.`;
122
+ }
123
+ return `\`${legacyKey}: ${renderValue(before)}\` is now written \`${migratedKey}: ${renderValue(after)}\`.`;
124
+ }
125
+ function lastSegment(path) {
126
+ const last = path[path.length - 1];
127
+ return typeof last === "number" ? `[${last}]` : String(last ?? "");
128
+ }
129
+ function renderValue(value) {
130
+ if (typeof value === "string")
131
+ return value;
132
+ if (value === null || value === undefined)
133
+ return "null";
134
+ if (typeof value === "number" || typeof value === "boolean")
135
+ return String(value);
136
+ if (isTaggedSentinel(value))
137
+ return `!${value.engine} ${value.source}`;
138
+ try {
139
+ return JSON.stringify(value) ?? String(value);
140
+ }
141
+ catch {
142
+ return String(value);
143
+ }
144
+ }