@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.
- package/dist/analyzer.d.ts +5 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +232 -94
- 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 +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- 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/manifest-visitor.d.ts +4 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +3 -3
- 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/module-file-claims.d.ts +65 -0
- package/dist/module-file-claims.d.ts.map +1 -0
- package/dist/module-file-claims.js +106 -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 +181 -54
- 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 +51 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +29 -3
- package/dist/validate-include-placement.d.ts +26 -0
- package/dist/validate-include-placement.d.ts.map +1 -0
- package/dist/validate-include-placement.js +67 -0
- 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-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +15 -12
- 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 +4 -3
- package/src/analyzer.ts +276 -129
- package/src/cel-environment.ts +6 -3
- package/src/definition-registry.ts +10 -0
- package/src/index.ts +31 -3
- package/src/invocation-contract.ts +43 -48
- package/src/loaded-types.ts +13 -0
- package/src/manifest-loader.ts +37 -6
- package/src/manifest-visitor.ts +11 -3
- 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/module-file-claims.ts +168 -0
- package/src/parse-loaded-file.ts +31 -12
- package/src/resolve-schema-type-refs.ts +93 -16
- package/src/schema-compat.ts +218 -62
- package/src/schema-region.ts +46 -0
- package/src/types.ts +54 -0
- package/src/validate-cel-context.ts +36 -3
- package/src/validate-include-placement.ts +70 -0
- package/src/validate-step-inputs.ts +56 -1
- package/src/validate-throws-coverage.ts +16 -11
- 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
package/src/index.ts
CHANGED
|
@@ -65,7 +65,7 @@ export {
|
|
|
65
65
|
PERMISSIVE_CONTRACT,
|
|
66
66
|
resolveContract,
|
|
67
67
|
resolveContractSchema,
|
|
68
|
-
|
|
68
|
+
withLiveValuesSkipped,
|
|
69
69
|
} from "./invocation-contract.js";
|
|
70
70
|
export type { ContractOrigin, ContractScope, ResolvedContract } from "./invocation-contract.js";
|
|
71
71
|
export {
|
|
@@ -97,7 +97,18 @@ export {
|
|
|
97
97
|
transfersControl,
|
|
98
98
|
} from "./ref-slot.js";
|
|
99
99
|
export type { RefSlot, RefUse, RefUseCases } from "./ref-slot.js";
|
|
100
|
-
export {
|
|
100
|
+
export {
|
|
101
|
+
ANNOTATION_KEYWORDS,
|
|
102
|
+
registerTeloKeywords,
|
|
103
|
+
valueTypeKeyword,
|
|
104
|
+
} from "./value-type-keyword.js";
|
|
105
|
+
export {
|
|
106
|
+
applyTextEdits,
|
|
107
|
+
isPlainSafe,
|
|
108
|
+
quoteStyleOf,
|
|
109
|
+
renderFixReplacement,
|
|
110
|
+
} from "./yaml-source-edit.js";
|
|
111
|
+
export type { QuoteStyle, TextEdit } from "./yaml-source-edit.js";
|
|
101
112
|
export {
|
|
102
113
|
hasProvidesZone,
|
|
103
114
|
hasRequiresZone,
|
|
@@ -123,6 +134,10 @@ export { validateZoneSlotDeclarations } from "./validate-zone-slots.js";
|
|
|
123
134
|
export type { ZoneSlotIssue } from "./validate-zone-slots.js";
|
|
124
135
|
export { validateDynamicSelectors, validateRefSlotDeclarations } from "./validate-ref-slots.js";
|
|
125
136
|
export type { RefSlotIssue } from "./validate-ref-slots.js";
|
|
137
|
+
export { validateValueTypeSlots } from "./validate-value-type-slots.js";
|
|
138
|
+
export type { ValueTypeSlotIssue } from "./validate-value-type-slots.js";
|
|
139
|
+
export { checkSchemaCompatibility } from "./schema-compat.js";
|
|
140
|
+
export type { CompatibilityResult } from "./schema-compat.js";
|
|
126
141
|
export { visitManifest } from "./manifest-visitor.js";
|
|
127
142
|
export type {
|
|
128
143
|
CelSiteEvent,
|
|
@@ -139,6 +154,15 @@ export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
|
|
139
154
|
export type { ModuleKind } from "./module-kinds.js";
|
|
140
155
|
export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
141
156
|
export type { ParseOptions } from "./parse-loaded-file.js";
|
|
157
|
+
export {
|
|
158
|
+
CORE_MIGRATIONS,
|
|
159
|
+
migrateFileText,
|
|
160
|
+
migrateManifests,
|
|
161
|
+
NO_MIGRATIONS,
|
|
162
|
+
parseMigrationEntry,
|
|
163
|
+
remapMigratedPaths,
|
|
164
|
+
} from "./migrations/index.js";
|
|
165
|
+
export type { FileMigrations, MigrationEntry, MigrationRewrite } from "./migrations/index.js";
|
|
142
166
|
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
143
167
|
export type { SyntheticImport } from "./inline-imports.js";
|
|
144
168
|
export {
|
|
@@ -207,6 +231,8 @@ export type {
|
|
|
207
231
|
PlatformAxis,
|
|
208
232
|
PlatformTarget,
|
|
209
233
|
} from "./artifact-selector.js";
|
|
234
|
+
export { collectModuleFileClaims } from "./module-file-claims.js";
|
|
235
|
+
export type { ModuleFileClaim } from "./module-file-claims.js";
|
|
210
236
|
export {
|
|
211
237
|
LayerIndexError,
|
|
212
238
|
matchControllerLayers,
|
|
@@ -224,10 +250,12 @@ export { documentToAst, parseToAst } from "./yaml-ast.js";
|
|
|
224
250
|
export type { AstDocument, AstMap, AstNode, AstPair, AstScalar, AstSeq } from "./yaml-ast.js";
|
|
225
251
|
export { CelParseError, buildCelSegments, wrapCelAst } from "./cel-ast.js";
|
|
226
252
|
export type { CelNode, CelSegment } from "./cel-ast.js";
|
|
227
|
-
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
|
253
|
+
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity, diagnosticFix } from "./types.js";
|
|
228
254
|
export type {
|
|
229
255
|
AnalysisDiagnostic,
|
|
230
256
|
AnalysisOptions,
|
|
257
|
+
DiagnosticData,
|
|
258
|
+
DiagnosticFix,
|
|
231
259
|
LoaderInitOptions,
|
|
232
260
|
LoadOptions,
|
|
233
261
|
ManifestSource,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { isLiveSlot, type ResourceDefinition } from "@telorun/sdk";
|
|
2
2
|
import {
|
|
3
3
|
type ContractDirection,
|
|
4
4
|
contractDeclarer,
|
|
@@ -108,33 +108,41 @@ export function resolveContractSchema(
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* A copy of `schema` with every `
|
|
112
|
-
*
|
|
111
|
+
* A copy of `schema` with every `live`-typed node left unconstrained, for
|
|
112
|
+
* validating a runtime value against.
|
|
113
113
|
*
|
|
114
|
-
*
|
|
115
|
-
* `inputType` and lists it in `required`, and `Record.Stream`, `Ai`, `Tar`
|
|
116
|
-
* `Console` do the same — so a one-directional skip would walk a live
|
|
117
|
-
* with AJV on the hottest path in the runtime. That is the same defect
|
|
118
|
-
* `stripCompiledValues` walking a live `ResourceInstance` in a ref slot: a
|
|
119
|
-
* object in a declared slot is not data to be traversed.
|
|
120
|
-
*
|
|
114
|
+
* Live values travel in BOTH directions — `Codec.Encoder` declares a stream on
|
|
115
|
+
* its `inputType` and lists it in `required`, and `Record.Stream`, `Ai`, `Tar`
|
|
116
|
+
* and `Console` do the same — so a one-directional skip would walk a live
|
|
117
|
+
* `Stream` with AJV on the hottest path in the runtime. That is the same defect
|
|
118
|
+
* as `stripCompiledValues` walking a live `ResourceInstance` in a ref slot: a
|
|
119
|
+
* live object in a declared slot is not data to be traversed.
|
|
120
|
+
*
|
|
121
|
+
* EXEMPTION IS A PROPERTY OF THE TYPE, not of a position. This used to neutralize
|
|
122
|
+
* only a key it found in a `properties` map, so an array-OF-streams element was
|
|
123
|
+
* reached and left constrained even though the walk descended into `items`.
|
|
124
|
+
* Reading the exemption off the declared value type makes an item, a union branch
|
|
125
|
+
* and a property the same case, and it is one rule instead of three.
|
|
126
|
+
*
|
|
127
|
+
* The exemption is from VALIDATION, never from TYPING: a live type's declared
|
|
128
|
+
* arguments still travel through every schema-typing walk the analyzer performs.
|
|
121
129
|
*
|
|
122
130
|
* Structural (returns a new object, never mutates), and shared so the analyzer
|
|
123
131
|
* and the kernel exempt the same set.
|
|
124
132
|
*/
|
|
125
|
-
export function
|
|
133
|
+
export function withLiveValuesSkipped(
|
|
126
134
|
schema: Record<string, any>,
|
|
127
135
|
/** Resolves a `$ref` to the schema it names. Required to see through the
|
|
128
136
|
* reference form the runtime deliberately KEEPS intact for its validator: a
|
|
129
137
|
* contract written as `{ $ref: "telo:mod/Type" }` has none of its own
|
|
130
138
|
* properties, so a walk that cannot follow the reference exempts nothing and
|
|
131
|
-
* the
|
|
139
|
+
* the live value is traversed after all. */
|
|
132
140
|
resolveRef?: (ref: string) => Record<string, any> | undefined,
|
|
133
141
|
): Record<string, any> {
|
|
134
|
-
return
|
|
142
|
+
return stripLive(schema, [], resolveRef);
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
function
|
|
145
|
+
function stripLive(
|
|
138
146
|
node: unknown,
|
|
139
147
|
// A PATH-scoped guard, not a global memo: a schema object reached twice from
|
|
140
148
|
// different parents must be stripped twice (a global `seen` would hand the
|
|
@@ -145,7 +153,7 @@ function stripStreams(
|
|
|
145
153
|
if (Array.isArray(node)) {
|
|
146
154
|
let changed = false;
|
|
147
155
|
const items = node.map((item) => {
|
|
148
|
-
const next =
|
|
156
|
+
const next = stripLive(item, path, resolveRef);
|
|
149
157
|
if (next !== item) changed = true;
|
|
150
158
|
return next;
|
|
151
159
|
});
|
|
@@ -155,6 +163,14 @@ function stripStreams(
|
|
|
155
163
|
let schema = node as Record<string, any>;
|
|
156
164
|
if (path.includes(schema)) return schema;
|
|
157
165
|
|
|
166
|
+
// The one rule. An empty schema leaves the node DECLARED but unconstrained,
|
|
167
|
+
// rather than deleted: deleting a property would force `additionalProperties:
|
|
168
|
+
// false` open, and a closed contract would stop rejecting unknown keys the
|
|
169
|
+
// moment it grew a stream — trading one exemption for a hole across the whole
|
|
170
|
+
// shape. `required` is untouched for the same reason: a live value IS present,
|
|
171
|
+
// it is only not walked.
|
|
172
|
+
if (isLiveSlot(schema)) return {};
|
|
173
|
+
|
|
158
174
|
// Follow a whole-document reference to SEE the annotations behind it, but
|
|
159
175
|
// return the original node when nothing behind it was stripped. Substituting
|
|
160
176
|
// the resolved target unconditionally would break schema identity — the
|
|
@@ -163,7 +179,7 @@ function stripStreams(
|
|
|
163
179
|
if (resolveRef && typeof schema.$ref === "string") {
|
|
164
180
|
const target = resolveRef(schema.$ref);
|
|
165
181
|
if (target && !path.includes(target)) {
|
|
166
|
-
const stripped =
|
|
182
|
+
const stripped = stripLive(target, [...path, schema], resolveRef);
|
|
167
183
|
if (stripped === target) return node;
|
|
168
184
|
const { $ref: _ref, ...siblings } = schema;
|
|
169
185
|
return Object.keys(siblings).length > 0 ? { ...stripped, ...siblings } : stripped;
|
|
@@ -171,44 +187,23 @@ function stripStreams(
|
|
|
171
187
|
}
|
|
172
188
|
const here = [...path, schema];
|
|
173
189
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// is expressed before the branches are merged), so the marked set is read
|
|
179
|
-
// from the folded view while the removal is applied here.
|
|
180
|
-
const streamed = Object.keys(properties).filter(
|
|
181
|
-
(key) => (properties[key] as Record<string, any> | undefined)?.["x-telo-stream"],
|
|
182
|
-
);
|
|
183
|
-
if (streamed.length > 0) {
|
|
184
|
-
const kept: Record<string, any> = {};
|
|
185
|
-
for (const [key, value] of Object.entries(properties)) {
|
|
186
|
-
if (!streamed.includes(key)) kept[key] = value;
|
|
187
|
-
}
|
|
188
|
-
// The key stays DECLARED but unconstrained, rather than being deleted.
|
|
189
|
-
// Deleting it would force `additionalProperties: false` open, and a closed
|
|
190
|
-
// contract would stop rejecting unknown keys the moment it grew a stream —
|
|
191
|
-
// trading one exemption for a hole across the whole shape.
|
|
192
|
-
for (const key of streamed) kept[key] = {};
|
|
193
|
-
out = { ...schema, properties: kept };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Recurse: a stream one level down (an item, a branch, a nested object) is as
|
|
198
|
-
// live as one at the root, and walking it with AJV is the same defect.
|
|
190
|
+
// Recurse: a live value one level down (a property, an item, a branch, a
|
|
191
|
+
// nested object) is as live as one at the root, and walking it with AJV is the
|
|
192
|
+
// same defect. Each is neutralized by the single rule above when the walk
|
|
193
|
+
// reaches it.
|
|
199
194
|
//
|
|
200
195
|
// `properties` and `$defs` are MAPS of schemas, not schemas — descending into
|
|
201
196
|
// them as if they were would visit nothing, since a map has none of the
|
|
202
197
|
// keywords this walk looks for.
|
|
203
|
-
let changed =
|
|
204
|
-
const result: Record<string, any> = { ...
|
|
198
|
+
let changed = false;
|
|
199
|
+
const result: Record<string, any> = { ...schema };
|
|
205
200
|
for (const key of ["properties", "$defs"] as const) {
|
|
206
|
-
const map =
|
|
201
|
+
const map = schema[key] as Record<string, any> | undefined;
|
|
207
202
|
if (!map || typeof map !== "object") continue;
|
|
208
203
|
let mapChanged = false;
|
|
209
204
|
const next: Record<string, any> = {};
|
|
210
205
|
for (const [name, child] of Object.entries(map)) {
|
|
211
|
-
const stripped =
|
|
206
|
+
const stripped = stripLive(child, here, resolveRef);
|
|
212
207
|
if (stripped !== child) mapChanged = true;
|
|
213
208
|
next[name] = stripped;
|
|
214
209
|
}
|
|
@@ -218,9 +213,9 @@ function stripStreams(
|
|
|
218
213
|
}
|
|
219
214
|
}
|
|
220
215
|
for (const key of ["items", "allOf", "anyOf", "oneOf"] as const) {
|
|
221
|
-
const child =
|
|
216
|
+
const child = schema[key];
|
|
222
217
|
if (child === undefined) continue;
|
|
223
|
-
const next =
|
|
218
|
+
const next = stripLive(child, here, resolveRef);
|
|
224
219
|
if (next !== child) {
|
|
225
220
|
result[key] = next;
|
|
226
221
|
changed = true;
|
|
@@ -237,7 +232,7 @@ function stripStreams(
|
|
|
237
232
|
* schema's defaults rather than by the size of the payload. */
|
|
238
233
|
export function defaultBearingPaths(
|
|
239
234
|
schema: Record<string, any>,
|
|
240
|
-
/** See {@link
|
|
235
|
+
/** See {@link withLiveValuesSkipped} — a contract kept in `$ref` form
|
|
241
236
|
* declares its defaults behind the reference, and a walk that cannot follow
|
|
242
237
|
* it would report none, leaving the caller's data shared where a fill lands. */
|
|
243
238
|
resolveRef?: (ref: string) => Record<string, any> | undefined,
|
package/src/loaded-types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import type { Document } from "yaml";
|
|
3
|
+
import type { FileMigrations } from "./migrations/driver.js";
|
|
3
4
|
import type { DocumentPosition } from "./position-metadata.js";
|
|
4
5
|
import type { AnalysisDiagnostic, Range } from "./types.js";
|
|
5
6
|
import type { AstDocument } from "./yaml-ast.js";
|
|
@@ -31,6 +32,12 @@ export interface LoadedFile {
|
|
|
31
32
|
positions: DocumentPosition[];
|
|
32
33
|
/** Document-level parse errors aggregated from `yaml.Document.errors`. */
|
|
33
34
|
parseErrors: ParseError[];
|
|
35
|
+
/** What the migration phase rewrote in this file, and the diagnostics for it.
|
|
36
|
+
* Empty unless the load opted into `migrate` AND a legacy spelling matched.
|
|
37
|
+
* `manifests` above are the POST-migration tree; `documents` / `astDocuments`
|
|
38
|
+
* / `positions` / `text` are always the author's file untouched, which is
|
|
39
|
+
* what the recorded `legacyPath` resolves against. */
|
|
40
|
+
migrations: FileMigrations;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export interface ParseError {
|
|
@@ -93,6 +100,12 @@ export interface LoadedGraph {
|
|
|
93
100
|
* it independently re-resolves an import (the analyzer already sees repointed
|
|
94
101
|
* `importEdges`). Empty when no module identity appeared at two sources. */
|
|
95
102
|
overrides: Map<string, string>;
|
|
103
|
+
/** Deprecation diagnostics from the loader's migration phase, scoped to the
|
|
104
|
+
* ENTRY's own module (owner + its `include:` partials). A migration rewrites
|
|
105
|
+
* always — the runtime must read artifacts published years ago — but reports
|
|
106
|
+
* only here, because a published dependency is not the consumer's to fix.
|
|
107
|
+
* Empty unless the load opted into `migrate`. */
|
|
108
|
+
migrationDiagnostics: AnalysisDiagnostic[];
|
|
96
109
|
/** Diagnostics produced while reconciling module versions — one per import
|
|
97
110
|
* edge redirected to a different version (warning for a same-major hoist,
|
|
98
111
|
* error for a major mismatch). Surfaced alongside `analyze()` diagnostics by
|
package/src/manifest-loader.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
LoadedModule,
|
|
10
10
|
} from "./loaded-types.js";
|
|
11
11
|
import { desugarLoadedFile } from "./inline-imports.js";
|
|
12
|
+
import type { MigrationEntry } from "./migrations/types.js";
|
|
12
13
|
import { isModuleKind } from "./module-kinds.js";
|
|
13
14
|
import { parseLoadedFile } from "./parse-loaded-file.js";
|
|
14
15
|
import { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
@@ -44,6 +45,18 @@ function collectParseDiagnostics(
|
|
|
44
45
|
return diagnostics;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/** Rewrite always, report locally. Every file in the graph was migrated, but a
|
|
49
|
+
* published dependency's manifest is not the consumer's to fix and its author
|
|
50
|
+
* is the only person who can — so only the entry module's own files (owner +
|
|
51
|
+
* its `include:` partials) report. Same rule `X_TELO_REF_UNRESOLVED` follows. */
|
|
52
|
+
function collectMigrationDiagnostics(entry: LoadedModule): AnalysisDiagnostic[] {
|
|
53
|
+
const diagnostics: AnalysisDiagnostic[] = [];
|
|
54
|
+
for (const file of [entry.owner, ...entry.partials]) {
|
|
55
|
+
diagnostics.push(...file.migrations.diagnostics);
|
|
56
|
+
}
|
|
57
|
+
return diagnostics;
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
const SYSTEM_KINDS = new Set([
|
|
48
61
|
"Telo.Application",
|
|
49
62
|
"Telo.Library",
|
|
@@ -51,12 +64,19 @@ const SYSTEM_KINDS = new Set([
|
|
|
51
64
|
"Telo.Definition",
|
|
52
65
|
]);
|
|
53
66
|
|
|
54
|
-
/** File cache variant tags: compile (c/r) × desugarImports (d/n)
|
|
55
|
-
* and a raw load of the same file are distinct entries so
|
|
56
|
-
* wrong manifest tree
|
|
57
|
-
|
|
67
|
+
/** File cache variant tags: compile (c/r) × desugarImports (d/n) × migrate
|
|
68
|
+
* (m/x). A desugared and a raw load of the same file are distinct entries so
|
|
69
|
+
* neither sees the wrong manifest tree, and the migration axis is there for
|
|
70
|
+
* the same reason — the editor's round-trip view and `telo migrate` must see
|
|
71
|
+
* the author's spelling, everything else the current one. */
|
|
72
|
+
const CACHE_VARIANTS = [
|
|
73
|
+
"rnx", "rdx", "cnx", "cdx",
|
|
74
|
+
"rnm", "rdm", "cnm", "cdm",
|
|
75
|
+
] as const;
|
|
58
76
|
function variantKey(options?: LoadOptions): string {
|
|
59
|
-
return `${options?.compile ? "c" : "r"}${options?.desugarImports ? "d" : "n"}
|
|
77
|
+
return `${options?.compile ? "c" : "r"}${options?.desugarImports ? "d" : "n"}${
|
|
78
|
+
options?.migrate ? "m" : "x"
|
|
79
|
+
}`;
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
export class Loader {
|
|
@@ -76,6 +96,7 @@ export class Loader {
|
|
|
76
96
|
|
|
77
97
|
protected sources: ManifestSource[];
|
|
78
98
|
private readonly celEnv: Environment;
|
|
99
|
+
private readonly migrations?: readonly MigrationEntry[];
|
|
79
100
|
|
|
80
101
|
/** Sources are resolved in order — the first whose `supports(url)` matches
|
|
81
102
|
* wins. The caller (composition root) decides which concrete sources exist
|
|
@@ -85,6 +106,7 @@ export class Loader {
|
|
|
85
106
|
constructor(sources: ManifestSource[] = [], options: LoaderInitOptions = {}) {
|
|
86
107
|
this.sources = [...sources];
|
|
87
108
|
this.celEnv = buildCelEnvironment(options.celHandlers);
|
|
109
|
+
this.migrations = options.migrations;
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
register(source: ManifestSource): this {
|
|
@@ -188,7 +210,13 @@ export class Loader {
|
|
|
188
210
|
* caller opted in. Desugaring lives here, not in the pure `parseLoadedFile`,
|
|
189
211
|
* so round-trip consumers (the editor) keep a raw manifest/AST/position
|
|
190
212
|
* triple they can pair by index; only resolved consumers that pass
|
|
191
|
-
* `desugarImports` see synthetic Telo.Import manifests.
|
|
213
|
+
* `desugarImports` see synthetic Telo.Import manifests.
|
|
214
|
+
*
|
|
215
|
+
* The migration phase runs inside `parseLoadedFile`, i.e. before desugaring —
|
|
216
|
+
* a rule must only ever match author-written nodes, and the position is what
|
|
217
|
+
* makes that structural rather than a convention. Nothing needs the later
|
|
218
|
+
* position: the `imports:` map is read straight off the module manifest and is
|
|
219
|
+
* equally available before desugaring. */
|
|
192
220
|
private parseAndMaybeDesugar(
|
|
193
221
|
source: string,
|
|
194
222
|
requestedUrl: string,
|
|
@@ -198,6 +226,8 @@ export class Loader {
|
|
|
198
226
|
const loaded = parseLoadedFile(source, requestedUrl, text, {
|
|
199
227
|
compile: options?.compile,
|
|
200
228
|
celEnv: this.celEnv,
|
|
229
|
+
migrate: options?.migrate,
|
|
230
|
+
migrations: this.migrations,
|
|
201
231
|
});
|
|
202
232
|
return options?.desugarImports ? desugarLoadedFile(loaded) : loaded;
|
|
203
233
|
}
|
|
@@ -364,6 +394,7 @@ export class Loader {
|
|
|
364
394
|
modules,
|
|
365
395
|
importEdges,
|
|
366
396
|
overrides,
|
|
397
|
+
migrationDiagnostics: collectMigrationDiagnostics(entry),
|
|
367
398
|
versionDiagnostics: diagnostics,
|
|
368
399
|
parseDiagnostics: collectParseDiagnostics(modules),
|
|
369
400
|
errors,
|
package/src/manifest-visitor.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
isRefSentinel,
|
|
4
|
+
isTaggedSentinel,
|
|
5
|
+
walkCelExpressions,
|
|
6
|
+
type CelSurface,
|
|
7
|
+
} from "@telorun/templating";
|
|
3
8
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
9
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
10
|
import {
|
|
@@ -114,6 +119,9 @@ export interface CelSiteEvent {
|
|
|
114
119
|
contextSchema?: Record<string, any>;
|
|
115
120
|
/** Scope of the matched context (e.g. `$.routes[*].handler`), if matched. */
|
|
116
121
|
matchedScope?: string;
|
|
122
|
+
/** Where `expr` sits in the scalar at `path`, and the delimiters to restore
|
|
123
|
+
* around a corrected expression. See `CelSurface`. */
|
|
124
|
+
surface: CelSurface;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
export interface ManifestVisitor {
|
|
@@ -352,7 +360,7 @@ export function visitManifest(
|
|
|
352
360
|
|
|
353
361
|
if (wantsCel) {
|
|
354
362
|
const contexts = definition?.schema ? extractContextsFromSchema(definition.schema) : [];
|
|
355
|
-
walkCelExpressions(r, "", (expr, path, engineName) => {
|
|
363
|
+
walkCelExpressions(r, "", (expr, path, engineName, surface) => {
|
|
356
364
|
let contextSchema: Record<string, any> | undefined;
|
|
357
365
|
let matchedScope: string | undefined;
|
|
358
366
|
for (const ctx of contexts) {
|
|
@@ -362,7 +370,7 @@ export function visitManifest(
|
|
|
362
370
|
break;
|
|
363
371
|
}
|
|
364
372
|
}
|
|
365
|
-
visitor.onCel!({ source: r, path, expr, engineName, contextSchema, matchedScope });
|
|
373
|
+
visitor.onCel!({ source: r, path, expr, engineName, contextSchema, matchedScope, surface });
|
|
366
374
|
});
|
|
367
375
|
}
|
|
368
376
|
|
|
@@ -0,0 +1,262 @@
|
|
|
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
|
+
|
|
16
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
17
|
+
import type { Document } from "yaml";
|
|
18
|
+
import {
|
|
19
|
+
applyEffectsToTree,
|
|
20
|
+
formatMigrationPath,
|
|
21
|
+
planPatch,
|
|
22
|
+
type MigrationEffect,
|
|
23
|
+
} from "./patch.js";
|
|
24
|
+
import { applyTextEdits, planTextEdits, type TextEdit } from "./yaml-edit.js";
|
|
25
|
+
import { applicableRules, buildMatchIndex, selectMatches } from "./match.js";
|
|
26
|
+
import { CORE_MIGRATIONS } from "./registry.js";
|
|
27
|
+
import { toDiagnostic, toRewrite, type AppliedPatch } from "./report.js";
|
|
28
|
+
import type {
|
|
29
|
+
MigrationEntry,
|
|
30
|
+
MigrationOperation,
|
|
31
|
+
MigrationPath,
|
|
32
|
+
MigrationRewrite,
|
|
33
|
+
} from "./types.js";
|
|
34
|
+
import type { AnalysisDiagnostic } from "../types.js";
|
|
35
|
+
|
|
36
|
+
/** What one file's migration produced. */
|
|
37
|
+
export interface FileMigrations {
|
|
38
|
+
/** One record per applied rewrite, in application order. Empty when the file
|
|
39
|
+
* carried no legacy spelling — the overwhelmingly common case. */
|
|
40
|
+
readonly rewrites: readonly MigrationRewrite[];
|
|
41
|
+
/** One diagnostic per rewrite. Whether they are SURFACED is the graph's
|
|
42
|
+
* decision: a migration rewrites always, because the runtime must read
|
|
43
|
+
* artifacts published years ago, but reports only for the entry's own
|
|
44
|
+
* modules, because a published dependency is not the consumer's to fix. */
|
|
45
|
+
readonly diagnostics: readonly AnalysisDiagnostic[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const NO_MIGRATIONS: FileMigrations = { rewrites: [], diagnostics: [] };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run `entries` over one file's parsed documents, mutating `manifests` in
|
|
52
|
+
* place. Returns the provenance records and their diagnostics.
|
|
53
|
+
*
|
|
54
|
+
* The manifests are the loader's own projection of the file, never the
|
|
55
|
+
* author's text — nothing here can reach disk.
|
|
56
|
+
*/
|
|
57
|
+
export function migrateManifests(args: {
|
|
58
|
+
source: string;
|
|
59
|
+
manifests: Array<ResourceManifest | null>;
|
|
60
|
+
entries?: readonly MigrationEntry[];
|
|
61
|
+
}): FileMigrations {
|
|
62
|
+
const entries = args.entries ?? CORE_MIGRATIONS;
|
|
63
|
+
if (entries.length === 0) return NO_MIGRATIONS;
|
|
64
|
+
|
|
65
|
+
const applied = applyAll(args.manifests, entries);
|
|
66
|
+
if (applied.length === 0) return NO_MIGRATIONS;
|
|
67
|
+
|
|
68
|
+
const rewrites = applied.map((a) => toRewrite(a));
|
|
69
|
+
const diagnostics = applied.map((a, i) =>
|
|
70
|
+
toDiagnostic(a, rewrites[i]!, args.source, args.manifests),
|
|
71
|
+
);
|
|
72
|
+
return { rewrites, diagnostics };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Migrate the author's YAML instead of the loader's tree — the operation
|
|
77
|
+
* `telo migrate` is the reference application of.
|
|
78
|
+
*
|
|
79
|
+
* `manifests` must be the RAW (un-migrated) projection of `documents`, since
|
|
80
|
+
* the matchers select legacy spellings. Returns `null` when nothing matched, so
|
|
81
|
+
* a caller can leave an untouched file untouched rather than rewriting
|
|
82
|
+
* identical bytes.
|
|
83
|
+
*
|
|
84
|
+
* `unwritable` names every rewrite the tree accepted but the TEXT could not
|
|
85
|
+
* express. The two appliers can disagree only in this direction, and the
|
|
86
|
+
* disagreement has to be reported: the diagnostic that sent the author here
|
|
87
|
+
* says "run `telo migrate`", so a location this silently skipped would keep
|
|
88
|
+
* warning with no way to act on it.
|
|
89
|
+
*/
|
|
90
|
+
export function migrateFileText(args: {
|
|
91
|
+
source: string;
|
|
92
|
+
text: string;
|
|
93
|
+
documents: readonly Document[];
|
|
94
|
+
manifests: ReadonlyArray<ResourceManifest | null>;
|
|
95
|
+
entries?: readonly MigrationEntry[];
|
|
96
|
+
}): {
|
|
97
|
+
text: string;
|
|
98
|
+
rewrites: MigrationRewrite[];
|
|
99
|
+
unwritable: MigrationRewrite[];
|
|
100
|
+
} | null {
|
|
101
|
+
const entries = args.entries ?? CORE_MIGRATIONS;
|
|
102
|
+
if (entries.length === 0) return null;
|
|
103
|
+
|
|
104
|
+
// Planned against a copy: the tree walk is what decides which patches apply
|
|
105
|
+
// (and in what order they refuse), so the YAML side must ask exactly the same
|
|
106
|
+
// question of exactly the same state rather than re-deriving it.
|
|
107
|
+
const scratch = args.manifests.map((m) => (m ? (structuredClone(m) as ResourceManifest) : null));
|
|
108
|
+
const applied = applyAll(scratch, entries);
|
|
109
|
+
if (applied.length === 0) return null;
|
|
110
|
+
|
|
111
|
+
const edits: TextEdit[] = [];
|
|
112
|
+
const kept: AppliedPatch[] = [];
|
|
113
|
+
const skipped: AppliedPatch[] = [];
|
|
114
|
+
for (const patch of applied) {
|
|
115
|
+
const doc = args.documents[patch.documentIndex];
|
|
116
|
+
if (!doc) {
|
|
117
|
+
skipped.push(patch);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const planned = planTextEdits(doc, args.text, patch.plan.effects);
|
|
121
|
+
// A patch the tree accepted but the text cannot express (a block scalar
|
|
122
|
+
// span, a flow-style entry) leaves that node alone rather than half-written.
|
|
123
|
+
if (!planned) {
|
|
124
|
+
skipped.push(patch);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Two patches whose spans overlap cannot both be spliced — the second would
|
|
128
|
+
// write into bytes the first replaced. The tree side has no equivalent,
|
|
129
|
+
// because an object write is idempotent where a splice is positional, so
|
|
130
|
+
// this is the one place the file applier is stricter: the later patch is
|
|
131
|
+
// dropped and its node stays as the author wrote it.
|
|
132
|
+
if (planned.some((edit) => edits.some((existing) => overlaps(edit, existing)))) {
|
|
133
|
+
skipped.push(patch);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
edits.push(...planned);
|
|
137
|
+
kept.push(patch);
|
|
138
|
+
}
|
|
139
|
+
if (edits.length === 0 && skipped.length === 0) return null;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
text: edits.length > 0 ? applyTextEdits(args.text, edits) : args.text,
|
|
143
|
+
rewrites: kept.map(toRewrite),
|
|
144
|
+
unwritable: skipped.map(toRewrite),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Whether two splices contend for the same bytes. A pure insertion (an empty
|
|
149
|
+
* span) collides only with a span that strictly contains its point. */
|
|
150
|
+
function overlaps(a: TextEdit, b: TextEdit): boolean {
|
|
151
|
+
if (a.start === a.end) return b.start < a.start && a.start < b.end;
|
|
152
|
+
if (b.start === b.end) return a.start < b.start && b.start < a.end;
|
|
153
|
+
return a.start < b.end && b.start < a.end;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** One pass: every match collected against the frozen pre-migration tree, then
|
|
157
|
+
* applied in entry order. */
|
|
158
|
+
function applyAll(
|
|
159
|
+
manifests: Array<ResourceManifest | null>,
|
|
160
|
+
entries: readonly MigrationEntry[],
|
|
161
|
+
): AppliedPatch[] {
|
|
162
|
+
const candidates: Array<{
|
|
163
|
+
entry: MigrationEntry;
|
|
164
|
+
ops: readonly MigrationOperation[];
|
|
165
|
+
documentIndex: number;
|
|
166
|
+
path: MigrationPath;
|
|
167
|
+
}> = [];
|
|
168
|
+
|
|
169
|
+
// One index per DOCUMENT, shared by every rule that can reach it — the walk
|
|
170
|
+
// is the expensive part and does not depend on the rule. This runs on the
|
|
171
|
+
// kernel's boot path for every file in the graph, so a walk per rule would
|
|
172
|
+
// scale the cost of loading any manifest with the size of the migration set.
|
|
173
|
+
//
|
|
174
|
+
// The `inKind` gate and the `under` regions are both known BEFORE the walk,
|
|
175
|
+
// so they bound it rather than filtering its output: a document no rule
|
|
176
|
+
// targets is never walked, and a region no rule names is never descended
|
|
177
|
+
// into. `type:` alone occurs a couple of hundred times in a standard-library
|
|
178
|
+
// manifest, so indexing sites that cannot be selected is the whole cost.
|
|
179
|
+
const flatRules = entries.flatMap((entry) =>
|
|
180
|
+
entry.rules.map((rule) => ({ entry, ops: rule.patch, match: rule.match })),
|
|
181
|
+
);
|
|
182
|
+
for (let documentIndex = 0; documentIndex < manifests.length; documentIndex++) {
|
|
183
|
+
const manifest = manifests[documentIndex];
|
|
184
|
+
if (!manifest) continue;
|
|
185
|
+
const { rules, keys, roots } = applicableRules(flatRules, manifest.kind);
|
|
186
|
+
if (rules.length === 0) continue;
|
|
187
|
+
const index = buildMatchIndex(manifest, keys, roots);
|
|
188
|
+
for (const { entry, ops, match } of rules) {
|
|
189
|
+
for (const path of selectMatches(index, manifest, match)) {
|
|
190
|
+
candidates.push({ entry, ops, documentIndex, path });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Arrays whose LENGTH an already-applied patch changed, as
|
|
196
|
+
// `<documentIndex>:<dotted path>`. A frozen match names a sequence element by
|
|
197
|
+
// INDEX, and an index is not an identity: once a sibling patch inserted or
|
|
198
|
+
// removed an item, the same index names a different element — including one
|
|
199
|
+
// another rule just produced, which is exactly what the frozen match set
|
|
200
|
+
// exists to prevent. A key-based path needs no such record, because a rename
|
|
201
|
+
// or removal makes the stale path resolve to nothing and `planPatch` refuses
|
|
202
|
+
// it on its own.
|
|
203
|
+
const shiftedArrays = new Set<string>();
|
|
204
|
+
|
|
205
|
+
const applied: AppliedPatch[] = [];
|
|
206
|
+
for (const candidate of candidates) {
|
|
207
|
+
const manifest = manifests[candidate.documentIndex];
|
|
208
|
+
if (!manifest) continue;
|
|
209
|
+
if (indexIsStale(candidate.documentIndex, candidate.path, shiftedArrays)) continue;
|
|
210
|
+
const result = planPatch(manifest, candidate.path, candidate.ops);
|
|
211
|
+
// Refusal is not an error: the node stays as the author wrote it and the
|
|
212
|
+
// ordinary validator reports it with an accurate message.
|
|
213
|
+
if (!result.ok) continue;
|
|
214
|
+
applyEffectsToTree(manifest, result.plan.effects);
|
|
215
|
+
for (const array of resizedArrays(candidate.documentIndex, result.plan.effects)) {
|
|
216
|
+
shiftedArrays.add(array);
|
|
217
|
+
}
|
|
218
|
+
applied.push({
|
|
219
|
+
entry: candidate.entry,
|
|
220
|
+
documentIndex: candidate.documentIndex,
|
|
221
|
+
matched: candidate.path,
|
|
222
|
+
plan: result.plan,
|
|
223
|
+
ops: candidate.ops,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return applied;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Arrays this patch resized, keyed for `shiftedArrays`. */
|
|
230
|
+
function resizedArrays(
|
|
231
|
+
documentIndex: number,
|
|
232
|
+
effects: readonly MigrationEffect[],
|
|
233
|
+
): string[] {
|
|
234
|
+
const out: string[] = [];
|
|
235
|
+
for (const effect of effects) {
|
|
236
|
+
if (effect.kind === "insert-item") {
|
|
237
|
+
out.push(`${documentIndex}:${formatMigrationPath(effect.path)}`);
|
|
238
|
+
} else if (
|
|
239
|
+
effect.kind === "remove-entry" &&
|
|
240
|
+
typeof effect.path[effect.path.length - 1] === "number"
|
|
241
|
+
) {
|
|
242
|
+
out.push(`${documentIndex}:${formatMigrationPath(effect.path.slice(0, -1))}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Whether any index along `path` steps into an array a prior patch resized. */
|
|
249
|
+
function indexIsStale(
|
|
250
|
+
documentIndex: number,
|
|
251
|
+
path: MigrationPath,
|
|
252
|
+
shiftedArrays: ReadonlySet<string>,
|
|
253
|
+
): boolean {
|
|
254
|
+
if (shiftedArrays.size === 0) return false;
|
|
255
|
+
for (let i = 0; i < path.length; i++) {
|
|
256
|
+
if (typeof path[i] !== "number") continue;
|
|
257
|
+
if (shiftedArrays.has(`${documentIndex}:${formatMigrationPath(path.slice(0, i))}`)) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|