@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,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";
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** The selector half of a migration: what a rule MATCHES, as data.
|
|
2
|
+
*
|
|
3
|
+
* A patch addresses a known location; a migration has to find *every*
|
|
4
|
+
* occurrence of a legacy spelling, so selection is the half a plain patch
|
|
5
|
+
* format has none of. It is declarative for the same reason the operations
|
|
6
|
+
* are: an entry travels to a Rust and a Go kernel, and a predicate expressed
|
|
7
|
+
* in one language would mean one artifact is read two ways — invisibly, since
|
|
8
|
+
* a migration that succeeds is silent.
|
|
9
|
+
*
|
|
10
|
+
* **Containment is POSITIVE and required.** A rule states which document kinds
|
|
11
|
+
* it may touch (`inKind`) and which region of those documents it may reach
|
|
12
|
+
* into (`under`); nothing outside is reachable. The alternative — walk
|
|
13
|
+
* everything and subtract — cannot be made sound, because the set to subtract
|
|
14
|
+
* is unbounded: a `Run.Value` value, an `Assert.Equals` expected, any kind
|
|
15
|
+
* whose config carries a user JSON blob can hold something shaped like the
|
|
16
|
+
* node a rule looks for, and forgetting one corrupts a manifest with no
|
|
17
|
+
* diagnostic. A denylist also cannot express the guarantee the module surface
|
|
18
|
+
* is promised to carry — *a dependency can rename its own field and provably
|
|
19
|
+
* nothing else* — which is a statement about what a rule may reach, so it has
|
|
20
|
+
* to be said positively. `notUnder` remains for subtracting inside a region a
|
|
21
|
+
* rule legitimately reaches, which is a narrowing, not the containment itself.
|
|
22
|
+
*
|
|
23
|
+
* **`under` is ANCHORED at the document root**, not a set of key names to look
|
|
24
|
+
* for anywhere on the path. Anchoring is what makes the containment claim
|
|
25
|
+
* true: a `Telo.Definition`'s `resources:` template body carries other kinds'
|
|
26
|
+
* configuration, and any of it may hold a key spelled `schema` over data that
|
|
27
|
+
* merely looks like a schema — so "some segment of the path is `schema`" would
|
|
28
|
+
* reach the very user JSON blob the positive form exists to keep out, and
|
|
29
|
+
* would delete from it silently. Anchored, `under` names top-level document
|
|
30
|
+
* keys and a region is a genuine subtree.
|
|
31
|
+
*
|
|
32
|
+
* **`inSchema` is the one region a kind list cannot name.** An annotation
|
|
33
|
+
* keyword occurs in author-written JSON Schema, and schema fragments are not
|
|
34
|
+
* confined to kind documents: an inline `inputType:` / `outputType:` on ANY kind
|
|
35
|
+
* that declares one, an API route's `request.schema.body`, a `Telo.JsonSchema`'s
|
|
36
|
+
* `schema`. That set of kinds is open — any kind may declare a schema-valued
|
|
37
|
+
* field — and enumerating the standard library's would put resource-kind
|
|
38
|
+
* knowledge into the analyzer, against the topology-driven constraint. So a rule
|
|
39
|
+
* may instead state that it reaches only inside a SCHEMA REGION, named by the
|
|
40
|
+
* KERNEL's own schema-valued keys, which no kind owns.
|
|
41
|
+
*
|
|
42
|
+
* With `inSchema`, and only with it, `inKind` / `under` may be `["*"]` — and
|
|
43
|
+
* only for a rule keyed on a reserved `x-telo-*` annotation. That pairing IS the
|
|
44
|
+
* containment: the region gate bounds where the walk may go, and the reserved-key
|
|
45
|
+
* rule bounds what it may touch, since an `x-telo-*` key is Telo vocabulary
|
|
46
|
+
* wherever it appears and cannot mean something else in someone's config. Both
|
|
47
|
+
* halves are refused at entry-read time, so a module-shipped entry can no more
|
|
48
|
+
* spell `"*"` than it can name another module's kind.
|
|
49
|
+
*
|
|
50
|
+
* The residue is stated rather than claimed away: a manifest that asserts ABOUT
|
|
51
|
+
* a schema — a schema literal under a key spelled `schema` inside an assertion's
|
|
52
|
+
* expected value — is reachable, and would be rewritten into its own synonym.
|
|
53
|
+
* That cannot be closed in a data-only matcher without naming kinds. It is
|
|
54
|
+
* accepted because the sites the wildcards reach are exactly the ones no
|
|
55
|
+
* enumeration covers, and the alternative leaves an author reading a deprecation
|
|
56
|
+
* `telo migrate` refuses to act on.
|
|
57
|
+
*
|
|
58
|
+
* The vocabulary is closed, which is what makes it a trust boundary once
|
|
59
|
+
* module-shipped entries are aggregated beside core ones. An unrecognized key
|
|
60
|
+
* is refused rather than ignored — a selector that silently matches wider than
|
|
61
|
+
* it reads is the one failure this cannot tolerate. */
|
|
62
|
+
import type { MigrationPath } from "./types.js";
|
|
63
|
+
/** The wildcard `inKind` / `under` value. Legal only alongside `inSchema`, and
|
|
64
|
+
* only for a rule keyed on an `x-telo-*` annotation. */
|
|
65
|
+
export declare const MATCH_ANY = "*";
|
|
66
|
+
export interface MigrationMatch {
|
|
67
|
+
/** The mapping key this rule rewrites. */
|
|
68
|
+
readonly key: string;
|
|
69
|
+
/** Document `kind:` values this rule may match in. Required and non-empty:
|
|
70
|
+
* a rule that does not say which documents it touches cannot be reasoned
|
|
71
|
+
* about, and is exactly the rule that reaches into a resource's config. */
|
|
72
|
+
readonly inKind: readonly string[];
|
|
73
|
+
/** The region of the document this rule may reach, named by TOP-LEVEL
|
|
74
|
+
* document keys: the matched node must be AT or BELOW one of them. Required
|
|
75
|
+
* and non-empty, for the same reason as `inKind`.
|
|
76
|
+
*
|
|
77
|
+
* Anchored at the root rather than matched anywhere on the path — see the
|
|
78
|
+
* file header; an unanchored `under` reaches into a nested resource's own
|
|
79
|
+
* configuration and is not containment at all.
|
|
80
|
+
*
|
|
81
|
+
* At-or-below rather than strictly-below so the vocabulary is complete — a
|
|
82
|
+
* rule that rewrites a top-level key names that key, instead of the region
|
|
83
|
+
* being unreachable and the grammar needing a second spelling for the
|
|
84
|
+
* document root. */
|
|
85
|
+
readonly under: readonly string[];
|
|
86
|
+
/** The value must deep-equal this. Mutually exclusive with `valueOneOf`. */
|
|
87
|
+
readonly value?: unknown;
|
|
88
|
+
/** The value must be one of these. Matching against the KNOWN legacy values
|
|
89
|
+
* rather than any value is what leaves an unrecognized one alone for the
|
|
90
|
+
* ordinary validator to report, instead of silently rewriting it. */
|
|
91
|
+
readonly valueOneOf?: readonly unknown[];
|
|
92
|
+
/** A key that must be present in the same mapping. The matched key is often
|
|
93
|
+
* stale only *because* of what sits beside it. */
|
|
94
|
+
readonly withSibling?: string;
|
|
95
|
+
/** Ancestor keys that disqualify a match inside the region `under` allows.
|
|
96
|
+
* The data-bearing JSON Schema keywords (`const`, `default`, `enum`,
|
|
97
|
+
* `examples`) hold values that may look like schemas. */
|
|
98
|
+
readonly notUnder?: readonly string[];
|
|
99
|
+
/** Narrow to a JSON Schema region: the match must be at or below a node
|
|
100
|
+
* reached through one of {@link SCHEMA_REGION_KEYS}.
|
|
101
|
+
*
|
|
102
|
+
* A second bound, never a replacement for `inKind` / `under` — it is what
|
|
103
|
+
* makes their wildcard forms safe, and it narrows an enumerated region just as
|
|
104
|
+
* usefully. See the file header for why an annotation rename needs it. */
|
|
105
|
+
readonly inSchema?: boolean;
|
|
106
|
+
}
|
|
107
|
+
export declare const MATCH_KEYS: readonly ["key", "inKind", "under", "value", "valueOneOf", "withSibling", "notUnder", "inSchema"];
|
|
108
|
+
/** Structural equality over JSON values — what `value` / `valueOneOf` compare
|
|
109
|
+
* with, so a match on `true` never also matches `"true"`.
|
|
110
|
+
*
|
|
111
|
+
* Exported because the PATCHER asks the same question in the other direction:
|
|
112
|
+
* "is the value already what this would write". Both answers have to come from
|
|
113
|
+
* one rule, or a rule could match a spelling the patch then declares current. */
|
|
114
|
+
export declare function deepEquals(a: unknown, b: unknown): boolean;
|
|
115
|
+
/** Read a rule's `match` block, refusing anything the vocabulary does not
|
|
116
|
+
* define. `describe` names the entry and rule so a failure says which. */
|
|
117
|
+
export declare function readMigrationMatch(describe: string, raw: unknown): MigrationMatch;
|
|
118
|
+
/** One candidate site: a mapping entry whose key some rule is interested in. */
|
|
119
|
+
interface MatchSite {
|
|
120
|
+
readonly path: MigrationPath;
|
|
121
|
+
readonly value: unknown;
|
|
122
|
+
readonly parent: Record<string, unknown>;
|
|
123
|
+
}
|
|
124
|
+
/** Every candidate site in one document, keyed by mapping key.
|
|
125
|
+
*
|
|
126
|
+
* Built ONCE per document and shared by every rule that can apply to it,
|
|
127
|
+
* because the walk is the expensive part and it does not depend on the rule —
|
|
128
|
+
* this is on the kernel's boot path for every file in the graph, so a walk per
|
|
129
|
+
* rule would scale the cost of loading any manifest with the size of the
|
|
130
|
+
* migration set.
|
|
131
|
+
*
|
|
132
|
+
* The walk is bounded by the same containment the rules declare: `roots` is
|
|
133
|
+
* the union of the applicable rules' `under`, so a region no rule can reach is
|
|
134
|
+
* never descended into and a document no rule can match is never walked at
|
|
135
|
+
* all. Only keys some rule asked for allocate a path array. */
|
|
136
|
+
export type MatchIndex = ReadonlyMap<string, readonly MatchSite[]>;
|
|
137
|
+
/**
|
|
138
|
+
* Index `document`'s candidate sites for `keys`, descending only into the
|
|
139
|
+
* top-level regions named by `roots`.
|
|
140
|
+
*
|
|
141
|
+
* The caller has already gated on `inKind` — see `selectMatches`, which repeats
|
|
142
|
+
* the check because it holds the individual rule.
|
|
143
|
+
*/
|
|
144
|
+
export declare function buildMatchIndex(document: unknown, keys: ReadonlySet<string>, roots: ReadonlySet<string>): MatchIndex;
|
|
145
|
+
/** The rules in `entries` that may match a document of `kind`, with the key and
|
|
146
|
+
* root sets their shared index needs. Empty rules mean the document is not
|
|
147
|
+
* walked at all. */
|
|
148
|
+
export declare function applicableRules<T extends {
|
|
149
|
+
readonly match: MigrationMatch;
|
|
150
|
+
}>(rules: Iterable<T>, kind: unknown): {
|
|
151
|
+
rules: T[];
|
|
152
|
+
keys: Set<string>;
|
|
153
|
+
roots: Set<string>;
|
|
154
|
+
};
|
|
155
|
+
/** The sites in `index` this match selects, in document order. */
|
|
156
|
+
export declare function selectMatches(index: MatchIndex, document: unknown, match: MigrationMatch): MigrationPath[];
|
|
157
|
+
/** Convenience for a single match against a document — builds a one-rule index.
|
|
158
|
+
* The driver uses `applicableRules` + `buildMatchIndex` + `selectMatches` so
|
|
159
|
+
* one walk serves every rule that can reach the document. */
|
|
160
|
+
export declare function findMatches(document: unknown, match: MigrationMatch): MigrationPath[];
|
|
161
|
+
export {};
|
|
162
|
+
//# sourceMappingURL=match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/migrations/match.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wDA4DwD;AAGxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;yDACyD;AACzD,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;gFAE4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;;;;;;;yBAWqB;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;0EAEsE;IACtE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IACzC;uDACmD;IACnD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;8DAE0D;IAC1D,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;+EAK2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,UAAU,mGASb,CAAC;AAMX;;;;;kFAKkF;AAClF,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAY1D;AAUD;2EAC2E;AAC3E,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,cAAc,CA6EjF;AAED,gFAAgF;AAChF,UAAU,SAAS;IACjB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED;;;;;;;;;;;gEAWgE;AAChE,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,CAAC;AAEnE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,OAAO,EACjB,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GACzB,UAAU,CA6CZ;AAED;;qBAEqB;AACrB,wBAAgB,eAAe,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;CAAE,EAC1E,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,EAClB,IAAI,EAAE,OAAO,GACZ;IAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAYvD;AAQD,kEAAkE;AAClE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,OAAO,EACjB,KAAK,EAAE,cAAc,GACpB,aAAa,EAAE,CA4BjB;AAED;;8DAE8D;AAC9D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,cAAc,GAAG,aAAa,EAAE,CAGrF"}
|