@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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +91 -9
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +6 -3
- package/dist/definition-registry.d.ts +5 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +9 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/invocation-contract.d.ts +21 -13
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +43 -46
- package/dist/loaded-types.d.ts +13 -0
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts +8 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +33 -6
- package/dist/migrations/driver.d.ts +69 -0
- package/dist/migrations/driver.d.ts.map +1 -0
- package/dist/migrations/driver.js +200 -0
- package/dist/migrations/entries/index.d.ts +3 -0
- package/dist/migrations/entries/index.d.ts.map +1 -0
- package/dist/migrations/entries/index.js +9 -0
- package/dist/migrations/entries/normalize-value-types.json +48 -0
- package/dist/migrations/entries/ref-slot-scalar-type.json +20 -0
- package/dist/migrations/entry-data.d.ts +25 -0
- package/dist/migrations/entry-data.d.ts.map +1 -0
- package/dist/migrations/entry-data.js +171 -0
- package/dist/migrations/index.d.ts +22 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +19 -0
- package/dist/migrations/match.d.ts +162 -0
- package/dist/migrations/match.d.ts.map +1 -0
- package/dist/migrations/match.js +295 -0
- package/dist/migrations/patch.d.ts +68 -0
- package/dist/migrations/patch.d.ts.map +1 -0
- package/dist/migrations/patch.js +236 -0
- package/dist/migrations/provenance.d.ts +25 -0
- package/dist/migrations/provenance.d.ts.map +1 -0
- package/dist/migrations/provenance.js +118 -0
- package/dist/migrations/registry.d.ts +35 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +35 -0
- package/dist/migrations/report.d.ts +28 -0
- package/dist/migrations/report.d.ts.map +1 -0
- package/dist/migrations/report.js +144 -0
- package/dist/migrations/types.d.ts +115 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +24 -0
- package/dist/migrations/yaml-edit.d.ts +21 -0
- package/dist/migrations/yaml-edit.d.ts.map +1 -0
- package/dist/migrations/yaml-edit.js +245 -0
- package/dist/parse-loaded-file.d.ts +8 -0
- package/dist/parse-loaded-file.d.ts.map +1 -1
- package/dist/parse-loaded-file.js +22 -13
- package/dist/resolve-schema-type-refs.d.ts +29 -12
- package/dist/resolve-schema-type-refs.d.ts.map +1 -1
- package/dist/resolve-schema-type-refs.js +89 -16
- package/dist/schema-compat.d.ts +35 -9
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +180 -64
- package/dist/schema-region.d.ts +33 -0
- package/dist/schema-region.d.ts.map +1 -0
- package/dist/schema-region.js +45 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +29 -3
- package/dist/validate-step-inputs.d.ts +8 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +47 -2
- package/dist/validate-value-type-slots.d.ts +43 -0
- package/dist/validate-value-type-slots.d.ts.map +1 -0
- package/dist/validate-value-type-slots.js +142 -0
- package/dist/value-type-keyword.d.ts +60 -0
- package/dist/value-type-keyword.d.ts.map +1 -0
- package/dist/value-type-keyword.js +119 -0
- package/dist/yaml-source-edit.d.ts +56 -0
- package/dist/yaml-source-edit.d.ts.map +1 -0
- package/dist/yaml-source-edit.js +93 -0
- package/package.json +3 -3
- package/src/analyzer.ts +99 -6
- package/src/cel-environment.ts +6 -3
- package/src/definition-registry.ts +10 -0
- package/src/index.ts +26 -2
- package/src/invocation-contract.ts +43 -48
- package/src/loaded-types.ts +13 -0
- package/src/manifest-loader.ts +37 -6
- package/src/migrations/driver.ts +262 -0
- package/src/migrations/entries/index.ts +10 -0
- package/src/migrations/entries/normalize-value-types.json +48 -0
- package/src/migrations/entries/ref-slot-scalar-type.json +20 -0
- package/src/migrations/entry-data.ts +207 -0
- package/src/migrations/index.ts +22 -0
- package/src/migrations/match.ts +382 -0
- package/src/migrations/patch.ts +265 -0
- package/src/migrations/provenance.ts +167 -0
- package/src/migrations/registry.ts +40 -0
- package/src/migrations/report.ts +169 -0
- package/src/migrations/types.ts +144 -0
- package/src/migrations/yaml-edit.ts +260 -0
- package/src/parse-loaded-file.ts +31 -12
- package/src/resolve-schema-type-refs.ts +93 -16
- package/src/schema-compat.ts +212 -74
- package/src/schema-region.ts +46 -0
- package/src/types.ts +17 -0
- package/src/validate-cel-context.ts +36 -3
- package/src/validate-step-inputs.ts +56 -1
- package/src/validate-value-type-slots.ts +182 -0
- package/src/value-type-keyword.ts +135 -0
- package/src/yaml-source-edit.ts +106 -0
- package/dist/binary-slot.d.ts +0 -39
- package/dist/binary-slot.d.ts.map +0 -1
- package/dist/binary-slot.js +0 -58
- 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"}
|