@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,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
+ }
@@ -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"}