@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,200 @@
1
+ /** The migration driver — one pass, one set of guarantees.
2
+ *
3
+ * **Composition is the driver's guarantee, not each entry's proof
4
+ * obligation.** The match set is frozen against the pre-migration tree, so no
5
+ * rule can match a node another rule produced; rules within an entry apply in
6
+ * order at each match; entries never see one another's output. That matters
7
+ * once core and module entries are aggregated from different parties, where
8
+ * "it happens to work" is not determinism. Idempotency then follows from the
9
+ * driver rather than from every author getting it right: a rule matches only
10
+ * the legacy spelling, and re-running finds nothing.
11
+ *
12
+ * How a rewrite is REPORTED — the provenance record and the diagnostic — is
13
+ * `report.ts`. Nothing there can change what a migration does, and nothing
14
+ * here decides how it reads. */
15
+ import { applyEffectsToTree, formatMigrationPath, planPatch, } from "./patch.js";
16
+ import { applyTextEdits, planTextEdits } from "./yaml-edit.js";
17
+ import { applicableRules, buildMatchIndex, selectMatches } from "./match.js";
18
+ import { CORE_MIGRATIONS } from "./registry.js";
19
+ import { toDiagnostic, toRewrite } from "./report.js";
20
+ export const NO_MIGRATIONS = { rewrites: [], diagnostics: [] };
21
+ /**
22
+ * Run `entries` over one file's parsed documents, mutating `manifests` in
23
+ * place. Returns the provenance records and their diagnostics.
24
+ *
25
+ * The manifests are the loader's own projection of the file, never the
26
+ * author's text — nothing here can reach disk.
27
+ */
28
+ export function migrateManifests(args) {
29
+ const entries = args.entries ?? CORE_MIGRATIONS;
30
+ if (entries.length === 0)
31
+ return NO_MIGRATIONS;
32
+ const applied = applyAll(args.manifests, entries);
33
+ if (applied.length === 0)
34
+ return NO_MIGRATIONS;
35
+ const rewrites = applied.map((a) => toRewrite(a));
36
+ const diagnostics = applied.map((a, i) => toDiagnostic(a, rewrites[i], args.source, args.manifests));
37
+ return { rewrites, diagnostics };
38
+ }
39
+ /**
40
+ * Migrate the author's YAML instead of the loader's tree — the operation
41
+ * `telo migrate` is the reference application of.
42
+ *
43
+ * `manifests` must be the RAW (un-migrated) projection of `documents`, since
44
+ * the matchers select legacy spellings. Returns `null` when nothing matched, so
45
+ * a caller can leave an untouched file untouched rather than rewriting
46
+ * identical bytes.
47
+ *
48
+ * `unwritable` names every rewrite the tree accepted but the TEXT could not
49
+ * express. The two appliers can disagree only in this direction, and the
50
+ * disagreement has to be reported: the diagnostic that sent the author here
51
+ * says "run `telo migrate`", so a location this silently skipped would keep
52
+ * warning with no way to act on it.
53
+ */
54
+ export function migrateFileText(args) {
55
+ const entries = args.entries ?? CORE_MIGRATIONS;
56
+ if (entries.length === 0)
57
+ return null;
58
+ // Planned against a copy: the tree walk is what decides which patches apply
59
+ // (and in what order they refuse), so the YAML side must ask exactly the same
60
+ // question of exactly the same state rather than re-deriving it.
61
+ const scratch = args.manifests.map((m) => (m ? structuredClone(m) : null));
62
+ const applied = applyAll(scratch, entries);
63
+ if (applied.length === 0)
64
+ return null;
65
+ const edits = [];
66
+ const kept = [];
67
+ const skipped = [];
68
+ for (const patch of applied) {
69
+ const doc = args.documents[patch.documentIndex];
70
+ if (!doc) {
71
+ skipped.push(patch);
72
+ continue;
73
+ }
74
+ const planned = planTextEdits(doc, args.text, patch.plan.effects);
75
+ // A patch the tree accepted but the text cannot express (a block scalar
76
+ // span, a flow-style entry) leaves that node alone rather than half-written.
77
+ if (!planned) {
78
+ skipped.push(patch);
79
+ continue;
80
+ }
81
+ // Two patches whose spans overlap cannot both be spliced — the second would
82
+ // write into bytes the first replaced. The tree side has no equivalent,
83
+ // because an object write is idempotent where a splice is positional, so
84
+ // this is the one place the file applier is stricter: the later patch is
85
+ // dropped and its node stays as the author wrote it.
86
+ if (planned.some((edit) => edits.some((existing) => overlaps(edit, existing)))) {
87
+ skipped.push(patch);
88
+ continue;
89
+ }
90
+ edits.push(...planned);
91
+ kept.push(patch);
92
+ }
93
+ if (edits.length === 0 && skipped.length === 0)
94
+ return null;
95
+ return {
96
+ text: edits.length > 0 ? applyTextEdits(args.text, edits) : args.text,
97
+ rewrites: kept.map(toRewrite),
98
+ unwritable: skipped.map(toRewrite),
99
+ };
100
+ }
101
+ /** Whether two splices contend for the same bytes. A pure insertion (an empty
102
+ * span) collides only with a span that strictly contains its point. */
103
+ function overlaps(a, b) {
104
+ if (a.start === a.end)
105
+ return b.start < a.start && a.start < b.end;
106
+ if (b.start === b.end)
107
+ return a.start < b.start && b.start < a.end;
108
+ return a.start < b.end && b.start < a.end;
109
+ }
110
+ /** One pass: every match collected against the frozen pre-migration tree, then
111
+ * applied in entry order. */
112
+ function applyAll(manifests, entries) {
113
+ const candidates = [];
114
+ // One index per DOCUMENT, shared by every rule that can reach it — the walk
115
+ // is the expensive part and does not depend on the rule. This runs on the
116
+ // kernel's boot path for every file in the graph, so a walk per rule would
117
+ // scale the cost of loading any manifest with the size of the migration set.
118
+ //
119
+ // The `inKind` gate and the `under` regions are both known BEFORE the walk,
120
+ // so they bound it rather than filtering its output: a document no rule
121
+ // targets is never walked, and a region no rule names is never descended
122
+ // into. `type:` alone occurs a couple of hundred times in a standard-library
123
+ // manifest, so indexing sites that cannot be selected is the whole cost.
124
+ const flatRules = entries.flatMap((entry) => entry.rules.map((rule) => ({ entry, ops: rule.patch, match: rule.match })));
125
+ for (let documentIndex = 0; documentIndex < manifests.length; documentIndex++) {
126
+ const manifest = manifests[documentIndex];
127
+ if (!manifest)
128
+ continue;
129
+ const { rules, keys, roots } = applicableRules(flatRules, manifest.kind);
130
+ if (rules.length === 0)
131
+ continue;
132
+ const index = buildMatchIndex(manifest, keys, roots);
133
+ for (const { entry, ops, match } of rules) {
134
+ for (const path of selectMatches(index, manifest, match)) {
135
+ candidates.push({ entry, ops, documentIndex, path });
136
+ }
137
+ }
138
+ }
139
+ // Arrays whose LENGTH an already-applied patch changed, as
140
+ // `<documentIndex>:<dotted path>`. A frozen match names a sequence element by
141
+ // INDEX, and an index is not an identity: once a sibling patch inserted or
142
+ // removed an item, the same index names a different element — including one
143
+ // another rule just produced, which is exactly what the frozen match set
144
+ // exists to prevent. A key-based path needs no such record, because a rename
145
+ // or removal makes the stale path resolve to nothing and `planPatch` refuses
146
+ // it on its own.
147
+ const shiftedArrays = new Set();
148
+ const applied = [];
149
+ for (const candidate of candidates) {
150
+ const manifest = manifests[candidate.documentIndex];
151
+ if (!manifest)
152
+ continue;
153
+ if (indexIsStale(candidate.documentIndex, candidate.path, shiftedArrays))
154
+ continue;
155
+ const result = planPatch(manifest, candidate.path, candidate.ops);
156
+ // Refusal is not an error: the node stays as the author wrote it and the
157
+ // ordinary validator reports it with an accurate message.
158
+ if (!result.ok)
159
+ continue;
160
+ applyEffectsToTree(manifest, result.plan.effects);
161
+ for (const array of resizedArrays(candidate.documentIndex, result.plan.effects)) {
162
+ shiftedArrays.add(array);
163
+ }
164
+ applied.push({
165
+ entry: candidate.entry,
166
+ documentIndex: candidate.documentIndex,
167
+ matched: candidate.path,
168
+ plan: result.plan,
169
+ ops: candidate.ops,
170
+ });
171
+ }
172
+ return applied;
173
+ }
174
+ /** Arrays this patch resized, keyed for `shiftedArrays`. */
175
+ function resizedArrays(documentIndex, effects) {
176
+ const out = [];
177
+ for (const effect of effects) {
178
+ if (effect.kind === "insert-item") {
179
+ out.push(`${documentIndex}:${formatMigrationPath(effect.path)}`);
180
+ }
181
+ else if (effect.kind === "remove-entry" &&
182
+ typeof effect.path[effect.path.length - 1] === "number") {
183
+ out.push(`${documentIndex}:${formatMigrationPath(effect.path.slice(0, -1))}`);
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+ /** Whether any index along `path` steps into an array a prior patch resized. */
189
+ function indexIsStale(documentIndex, path, shiftedArrays) {
190
+ if (shiftedArrays.size === 0)
191
+ return false;
192
+ for (let i = 0; i < path.length; i++) {
193
+ if (typeof path[i] !== "number")
194
+ continue;
195
+ if (shiftedArrays.has(`${documentIndex}:${formatMigrationPath(path.slice(0, i))}`)) {
196
+ return true;
197
+ }
198
+ }
199
+ return false;
200
+ }
@@ -0,0 +1,3 @@
1
+ /** Every entry file, in the order the driver applies them. */
2
+ export declare const MIGRATION_ENTRY_FILES: ReadonlyArray<readonly [file: string, data: unknown]>;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/migrations/entries/index.ts"],"names":[],"mappings":"AAKA,8DAA8D;AAC9D,eAAO,MAAM,qBAAqB,EAAE,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAGvF,CAAC"}
@@ -0,0 +1,9 @@
1
+ // GENERATED by scripts/copy-migration-entries.mjs — do not edit, and do not commit.
2
+ // Source: analyzer/migrations/*.json (lexically ordered).
3
+ import e0 from "./normalize-value-types.json" with { type: "json" };
4
+ import e1 from "./ref-slot-scalar-type.json" with { type: "json" };
5
+ /** Every entry file, in the order the driver applies them. */
6
+ export const MIGRATION_ENTRY_FILES = [
7
+ ["normalize-value-types.json", e0],
8
+ ["ref-slot-scalar-type.json", e1],
9
+ ];
@@ -0,0 +1,48 @@
1
+ {
2
+ "$comment": "Three keywords answered one question — what is the value at this slot, beyond what JSON Schema's `type` can say — and each answered it differently. `inSchema` with the wildcard `inKind` / `under` is what makes this reachable at all: these annotations occur in author-written schema fragments inside ORDINARY resource documents (an inline `inputType:` on any kind that declares one, a route's `request.schema.body`), and that set of kinds is open, so enumerating it would be both incomplete and a violation of the topology-driven constraint. The wildcards are earned by the pairing the reader enforces: a schema region bounds where the walk may go, and an `x-telo-*` key — Telo vocabulary wherever it appears — bounds what it may touch. Each rule sets the value BEFORE renaming the key, so both match the frozen pre-migration tree; a rename first would leave the second rule looking for a node that no longer exists. `notUnder` keeps all three out of the data-bearing keywords, where a value may merely look like a schema.",
3
+ "id": "normalize-value-types",
4
+ "code": "X_TELO_TYPE_DEPRECATED",
5
+ "severity": "warning",
6
+ "reason": "The three value-type annotations were unified into one `x-telo-type`, whose names are `Telo.`-qualified like every other kernel-owned name. `x-telo-binary` and `x-telo-stream` said what a value IS just as `x-telo-type` did, differing only in posture toward the JSON Schema layer — so a fourth kind of value meant a fourth keyword and edits across four packages.",
7
+ "rules": [
8
+ {
9
+ "match": {
10
+ "key": "x-telo-stream",
11
+ "inKind": ["*"],
12
+ "under": ["*"],
13
+ "inSchema": true,
14
+ "value": true,
15
+ "notUnder": ["const", "default", "enum", "examples"]
16
+ },
17
+ "patch": [
18
+ { "op": "set-value", "value": "Telo.Stream" },
19
+ { "op": "rename-key", "to": "x-telo-type" }
20
+ ]
21
+ },
22
+ {
23
+ "match": {
24
+ "key": "x-telo-binary",
25
+ "inKind": ["*"],
26
+ "under": ["*"],
27
+ "inSchema": true,
28
+ "value": true,
29
+ "notUnder": ["const", "default", "enum", "examples"]
30
+ },
31
+ "patch": [
32
+ { "op": "set-value", "value": "Telo.Bytes" },
33
+ { "op": "rename-key", "to": "x-telo-type" }
34
+ ]
35
+ },
36
+ {
37
+ "match": {
38
+ "key": "x-telo-type",
39
+ "inKind": ["*"],
40
+ "under": ["*"],
41
+ "inSchema": true,
42
+ "valueOneOf": ["TcpPort", "UdpPort"],
43
+ "notUnder": ["const", "default", "enum", "examples"]
44
+ },
45
+ "patch": [{ "op": "set-value", "qualify": "Telo." }]
46
+ }
47
+ ]
48
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$comment": "See README.md in this directory. `inKind` + `under` are the positive containment: `under` names TOP-LEVEL keys, so this rule can reach nothing but the schema blocks of a kind document — never the `resources:` template body, whose nested config may hold a key spelled `schema` over data. `withSibling` is what makes it precise — a scalar `type:` is stale only because an `x-telo-ref` sits beside it. `notUnder` then narrows within that region, keeping the rule out of the data-bearing keywords where a value may merely look like a schema.",
3
+ "id": "ref-slot-scalar-type",
4
+ "code": "X_TELO_REF_SCALAR_TYPE",
5
+ "severity": "warning",
6
+ "reason": "A reference is written with the `!ref` tag and resolves to an object, so a reference slot no longer carries the scalar `type:` that plain-string references were pinned to. Leaving it in place makes the slot reject the very value it is declared to accept, and every schema reader has had to know to ignore it.",
7
+ "rules": [
8
+ {
9
+ "match": {
10
+ "key": "type",
11
+ "inKind": ["Telo.Definition", "Telo.Abstract"],
12
+ "under": ["schema", "status", "inputType", "outputType"],
13
+ "valueOneOf": ["string", "number", "integer", "boolean"],
14
+ "withSibling": "x-telo-ref",
15
+ "notUnder": ["const", "default", "enum", "examples"]
16
+ },
17
+ "patch": [{ "op": "remove-entry" }]
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,25 @@
1
+ /** The on-disk shape of a migration entry, and its reader.
2
+ *
3
+ * An entry is DATA: one file per entry under `analyzer/migrations/`, consumed
4
+ * as one lexically ordered set. That is what makes adding a migration one file
5
+ * and retiring one deleting it, what ships the set in the published package,
6
+ * and what lets the Rust crate embed the identical files at build time rather
7
+ * than reimplementing each rewrite.
8
+ *
9
+ * **An entry contains no code.** Both halves — what a rule matches and what it
10
+ * patches — are data, so one file is read identically by every kernel. A
11
+ * predicate expressed in one language would mean one artifact is read two
12
+ * ways, invisibly, since a migration that succeeds is silent.
13
+ *
14
+ * Reading is STRICT. A malformed entry is an authoring mistake, and the
15
+ * alternative to throwing is a migration that silently does not run — the one
16
+ * failure mode a rewrite-on-load design cannot afford, because a migration
17
+ * that succeeds and one that never fires look identical. */
18
+ import type { MigrationEntry } from "./types.js";
19
+ /**
20
+ * Read one entry file's parsed data into a `MigrationEntry`.
21
+ *
22
+ * `file` names the entry file, so a failure says which one.
23
+ */
24
+ export declare function parseMigrationEntry(file: string, data: unknown): MigrationEntry;
25
+ //# sourceMappingURL=entry-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-data.d.ts","sourceRoot":"","sources":["../../src/migrations/entry-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;6DAgB6D;AAI7D,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,YAAY,CAAC;AAgJpB;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,cAAc,CAiC/E"}
@@ -0,0 +1,171 @@
1
+ /** The on-disk shape of a migration entry, and its reader.
2
+ *
3
+ * An entry is DATA: one file per entry under `analyzer/migrations/`, consumed
4
+ * as one lexically ordered set. That is what makes adding a migration one file
5
+ * and retiring one deleting it, what ships the set in the published package,
6
+ * and what lets the Rust crate embed the identical files at build time rather
7
+ * than reimplementing each rewrite.
8
+ *
9
+ * **An entry contains no code.** Both halves — what a rule matches and what it
10
+ * patches — are data, so one file is read identically by every kernel. A
11
+ * predicate expressed in one language would mean one artifact is read two
12
+ * ways, invisibly, since a migration that succeeds is silent.
13
+ *
14
+ * Reading is STRICT. A malformed entry is an authoring mistake, and the
15
+ * alternative to throwing is a migration that silently does not run — the one
16
+ * failure mode a rewrite-on-load design cannot afford, because a migration
17
+ * that succeeds and one that never fires look identical. */
18
+ import { DiagnosticSeverity } from "../types.js";
19
+ import { readMigrationMatch } from "./match.js";
20
+ import { MIGRATION_OPS } from "./types.js";
21
+ /** How an entry file names a severity. The LSP integers are a transport detail
22
+ * of one editor; an entry is read by a Rust crate too. */
23
+ const SEVERITIES = {
24
+ error: DiagnosticSeverity.Error,
25
+ warning: DiagnosticSeverity.Warning,
26
+ info: DiagnosticSeverity.Information,
27
+ hint: DiagnosticSeverity.Hint,
28
+ };
29
+ class MigrationEntryError extends Error {
30
+ constructor(file, detail) {
31
+ super(`Invalid migration entry '${file}': ${detail}`);
32
+ this.name = "MigrationEntryError";
33
+ }
34
+ }
35
+ function isPlainObject(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function requireString(file, node, key) {
39
+ const value = node[key];
40
+ if (typeof value !== "string" || value.length === 0) {
41
+ throw new MigrationEntryError(file, `'${key}' must be a non-empty string`);
42
+ }
43
+ return value;
44
+ }
45
+ const OPERATION_KEYS = {
46
+ "rename-key": ["to"],
47
+ "set-value": ["value", "qualify"],
48
+ "set-tag": ["tag"],
49
+ "insert-item": ["value", "at"],
50
+ "remove-entry": [],
51
+ };
52
+ /** Top-level entry keys. `$comment` is the sanctioned place for author notes —
53
+ * the reader ignores it, but it has to be DECLARED, or "unknown keys are
54
+ * refused" would be true of every level but this one. */
55
+ const ENTRY_KEYS = ["id", "code", "severity", "reason", "rules", "$comment"];
56
+ /**
57
+ * A value a patch may write.
58
+ *
59
+ * Scalars only, because "every operation has a known YAML edit form" is what
60
+ * makes a migration applicable to a FILE, and the file applier renders a value
61
+ * by re-quoting it in the author's own style at the node's own span — which has
62
+ * no meaning for a mapping or a sequence. Without this check the limitation is
63
+ * invisible until a user runs `telo migrate` and is told, permanently, to fix
64
+ * it by hand; with it, the entry's author learns at authoring time. Structured
65
+ * values are a vocabulary extension (a block renderer), not a silent gap.
66
+ */
67
+ function requireScalarValue(file, index, op, value) {
68
+ if (value === null ||
69
+ typeof value === "string" ||
70
+ typeof value === "number" ||
71
+ typeof value === "boolean") {
72
+ return;
73
+ }
74
+ throw new MigrationEntryError(file, `patch[${index}] ('${op}') value must be a scalar (string, number, boolean or null) — ` +
75
+ `a mapping or sequence has no in-place YAML edit form, so \`telo migrate\` could never apply it`);
76
+ }
77
+ function readOperation(file, raw, index) {
78
+ if (!isPlainObject(raw)) {
79
+ throw new MigrationEntryError(file, `patch[${index}] must be a mapping`);
80
+ }
81
+ const op = raw.op;
82
+ if (typeof op !== "string" || !MIGRATION_OPS.includes(op)) {
83
+ throw new MigrationEntryError(file, `patch[${index}].op '${String(op)}' is not one of ${MIGRATION_OPS.join(", ")}`);
84
+ }
85
+ // The vocabulary is closed, so an unknown parameter is a typo — and a typo in
86
+ // a patch is a rewrite that quietly does something other than what it reads
87
+ // as. `set-value`'s two parameters are alternatives, checked below.
88
+ const allowed = new Set([...OPERATION_KEYS[op], "op"]);
89
+ for (const key of Object.keys(raw)) {
90
+ if (!allowed.has(key)) {
91
+ throw new MigrationEntryError(file, `patch[${index}] ('${op}') has no parameter '${key}'`);
92
+ }
93
+ }
94
+ switch (op) {
95
+ case "rename-key":
96
+ return { op, to: requireString(file, raw, "to") };
97
+ case "set-value": {
98
+ const hasValue = Object.hasOwn(raw, "value");
99
+ const hasQualify = Object.hasOwn(raw, "qualify");
100
+ if (hasValue === hasQualify) {
101
+ throw new MigrationEntryError(file, `patch[${index}] ('set-value') takes exactly one of 'value' or 'qualify'`);
102
+ }
103
+ if (hasQualify)
104
+ return { op, qualify: requireString(file, raw, "qualify") };
105
+ requireScalarValue(file, index, op, raw.value);
106
+ return { op, value: raw.value };
107
+ }
108
+ case "set-tag":
109
+ return { op, tag: requireString(file, raw, "tag") };
110
+ case "insert-item": {
111
+ if (!Object.hasOwn(raw, "value")) {
112
+ throw new MigrationEntryError(file, `patch[${index}] ('insert-item') requires 'value'`);
113
+ }
114
+ requireScalarValue(file, index, op, raw.value);
115
+ const at = raw.at;
116
+ if (at !== undefined && (typeof at !== "number" || !Number.isInteger(at) || at < 0)) {
117
+ throw new MigrationEntryError(file, `patch[${index}].at must be a non-negative integer when present`);
118
+ }
119
+ return at === undefined ? { op, value: raw.value } : { op, value: raw.value, at };
120
+ }
121
+ case "remove-entry":
122
+ return { op };
123
+ default:
124
+ throw new MigrationEntryError(file, `patch[${index}].op '${op}' is unhandled`);
125
+ }
126
+ }
127
+ function readRule(file, raw, index) {
128
+ if (!isPlainObject(raw)) {
129
+ throw new MigrationEntryError(file, `rules[${index}] must be a mapping`);
130
+ }
131
+ if (!Array.isArray(raw.patch) || raw.patch.length === 0) {
132
+ throw new MigrationEntryError(file, `rules[${index}].patch must be a non-empty sequence`);
133
+ }
134
+ return {
135
+ match: readMigrationMatch(`Invalid migration entry '${file}': rules[${index}]`, raw.match),
136
+ patch: raw.patch.map((op, i) => readOperation(file, op, i)),
137
+ };
138
+ }
139
+ /**
140
+ * Read one entry file's parsed data into a `MigrationEntry`.
141
+ *
142
+ * `file` names the entry file, so a failure says which one.
143
+ */
144
+ export function parseMigrationEntry(file, data) {
145
+ if (!isPlainObject(data)) {
146
+ throw new MigrationEntryError(file, "an entry must be a mapping");
147
+ }
148
+ // Closed at every level, this one included: a typo'd top-level key would
149
+ // otherwise be silently ignored, which is the one failure a rewrite-on-load
150
+ // design cannot afford once module-shipped entries make this a trust boundary.
151
+ for (const key of Object.keys(data)) {
152
+ if (!ENTRY_KEYS.includes(key)) {
153
+ throw new MigrationEntryError(file, `an entry has no key '${key}'. Known keys: ${ENTRY_KEYS.join(", ")}.`);
154
+ }
155
+ }
156
+ const severityName = requireString(file, data, "severity");
157
+ const severity = SEVERITIES[severityName];
158
+ if (severity === undefined) {
159
+ throw new MigrationEntryError(file, `severity '${severityName}' must be one of ${Object.keys(SEVERITIES).join(", ")}`);
160
+ }
161
+ if (!Array.isArray(data.rules) || data.rules.length === 0) {
162
+ throw new MigrationEntryError(file, "'rules' must be a non-empty sequence");
163
+ }
164
+ return {
165
+ id: requireString(file, data, "id"),
166
+ code: requireString(file, data, "code"),
167
+ severity,
168
+ reason: requireString(file, data, "reason").trim(),
169
+ rules: data.rules.map((rule, i) => readRule(file, rule, i)),
170
+ };
171
+ }
@@ -0,0 +1,22 @@
1
+ /** The migration mechanism's surface — what a consumer OUTSIDE this directory
2
+ * may hold.
3
+ *
4
+ * Deliberately narrow. The matcher, the patch planner and the two appliers are
5
+ * one mechanism with one entry point per job (`migrateManifests` for the
6
+ * loader, `migrateFileText` for `telo migrate`, `remapMigratedPaths` for a
7
+ * diagnostic consumer); re-exporting their internals would make the plan/apply
8
+ * split, the effect vocabulary and the text-edit shape semver-bound API with
9
+ * no caller, and freeze the one part most likely to change as the operation
10
+ * vocabulary grows.
11
+ *
12
+ * The entry-set trio (`CORE_MIGRATIONS`, `parseMigrationEntry`,
13
+ * `MigrationEntry`) is here because `LoaderInitOptions.migrations` is a
14
+ * composition-root seam: a host aggregating module-shipped entries beside the
15
+ * core ones needs to read one and to name the set it is extending. */
16
+ export { migrateFileText, migrateManifests, NO_MIGRATIONS } from "./driver.js";
17
+ export type { FileMigrations } from "./driver.js";
18
+ export { parseMigrationEntry } from "./entry-data.js";
19
+ export { remapMigratedPaths } from "./provenance.js";
20
+ export { CORE_MIGRATIONS } from "./registry.js";
21
+ export type { MigrationEntry, MigrationRewrite } from "./types.js";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;uEAcuE;AAEvE,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/E,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,19 @@
1
+ /** The migration mechanism's surface — what a consumer OUTSIDE this directory
2
+ * may hold.
3
+ *
4
+ * Deliberately narrow. The matcher, the patch planner and the two appliers are
5
+ * one mechanism with one entry point per job (`migrateManifests` for the
6
+ * loader, `migrateFileText` for `telo migrate`, `remapMigratedPaths` for a
7
+ * diagnostic consumer); re-exporting their internals would make the plan/apply
8
+ * split, the effect vocabulary and the text-edit shape semver-bound API with
9
+ * no caller, and freeze the one part most likely to change as the operation
10
+ * vocabulary grows.
11
+ *
12
+ * The entry-set trio (`CORE_MIGRATIONS`, `parseMigrationEntry`,
13
+ * `MigrationEntry`) is here because `LoaderInitOptions.migrations` is a
14
+ * composition-root seam: a host aggregating module-shipped entries beside the
15
+ * core ones needs to read one and to name the set it is extending. */
16
+ export { migrateFileText, migrateManifests, NO_MIGRATIONS } from "./driver.js";
17
+ export { parseMigrationEntry } from "./entry-data.js";
18
+ export { remapMigratedPaths } from "./provenance.js";
19
+ export { CORE_MIGRATIONS } from "./registry.js";