codex-plugin-doctor 1.72.0 → 1.73.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/README.md +2 -2
- package/dist/core/mcp-tool-impact.js +224 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -585,9 +585,9 @@ jobs:
|
|
|
585
585
|
runs-on: ubuntu-latest
|
|
586
586
|
steps:
|
|
587
587
|
- uses: actions/checkout@v5
|
|
588
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
588
|
+
- uses: Esquetta/CodexPluginDoctor@v1.73.0
|
|
589
589
|
with:
|
|
590
|
-
version: "1.
|
|
590
|
+
version: "1.73.0"
|
|
591
591
|
path: .
|
|
592
592
|
runtime: "true"
|
|
593
593
|
policy: codex-publish
|
|
@@ -27,13 +27,15 @@ const UNCLASSIFIED_REASONS = new Set([
|
|
|
27
27
|
// Keywords that document a schema without constraining accepted or produced values.
|
|
28
28
|
const DOCUMENTATION_KEYWORDS = new Set(["title", "description", "examples", "default", "deprecated", "$comment"]);
|
|
29
29
|
const ROOT_STRUCTURAL_KEYWORDS = new Set(["type", "properties", "required", "additionalProperties", "$schema"]);
|
|
30
|
-
const LOWER_BOUND_KEYWORDS = ["
|
|
31
|
-
const UPPER_BOUND_KEYWORDS = ["
|
|
30
|
+
const LOWER_BOUND_KEYWORDS = ["minLength", "minItems"];
|
|
31
|
+
const UPPER_BOUND_KEYWORDS = ["maxLength", "maxItems"];
|
|
32
|
+
// Numeric bounds, compared as one effective bound per side because the exclusive form may replace the inclusive one.
|
|
33
|
+
const NUMERIC_BOUND_KEYWORDS = ["minimum", "exclusiveMinimum", "maximum", "exclusiveMaximum"];
|
|
32
34
|
// String keywords that can only be introduced or removed with a known effect; a changed value is unclassified.
|
|
33
35
|
const STRING_CONSTRAINT_KEYWORDS = ["pattern", "format"];
|
|
34
36
|
const NESTED_STRUCTURAL_KEYWORDS = new Set([
|
|
35
|
-
"type", "enum", "const", "properties", "required", "additionalProperties", "items",
|
|
36
|
-
...LOWER_BOUND_KEYWORDS, ...UPPER_BOUND_KEYWORDS, ...STRING_CONSTRAINT_KEYWORDS
|
|
37
|
+
"type", "enum", "const", "properties", "required", "additionalProperties", "items", "multipleOf", "uniqueItems",
|
|
38
|
+
...LOWER_BOUND_KEYWORDS, ...UPPER_BOUND_KEYWORDS, ...NUMERIC_BOUND_KEYWORDS, ...STRING_CONSTRAINT_KEYWORDS
|
|
37
39
|
]);
|
|
38
40
|
// When every reference is a same-document fragment, definition containers are compared only
|
|
39
41
|
// where a resolved `$ref` reaches into them; otherwise they are compared as written.
|
|
@@ -45,11 +47,16 @@ const NESTED_STRUCTURAL_WITH_DEFINITIONS = new Set([...NESTED_STRUCTURAL_KEYWORD
|
|
|
45
47
|
const DATA_KEYWORDS = new Set(["enum", "const", "examples", "example", "default"]);
|
|
46
48
|
// Keywords whose values map names to subschemas; their keys are names, never keywords.
|
|
47
49
|
const NAME_MAP_KEYWORDS = new Set(["properties", "patternProperties", "$defs", "definitions", "dependentSchemas", "dependencies"]);
|
|
48
|
-
// Subschemas nested deeper than this below the root are not compared and stay unclassified
|
|
50
|
+
// Subschemas nested deeper than this below the root are not compared and stay unclassified; composition
|
|
51
|
+
// wrappers keep the depth of the schema they wrap and are bounded by MAX_WRAPPER_HOPS instead.
|
|
49
52
|
const MAX_NESTED_DEPTH = 8;
|
|
50
53
|
// Keywords that only hold or name subschemas and never validate the object's own keys, so a
|
|
51
54
|
// new property beside them stays compatible. Any other unclassified keyword may govern keys.
|
|
52
55
|
const DEFINITION_KEYWORDS = new Set(["$defs", "definitions", "$id", "$anchor", "$dynamicAnchor"]);
|
|
56
|
+
// `anyOf` alternatives beyond this many on either side are not paired.
|
|
57
|
+
const MAX_ALTERNATIVES = 32;
|
|
58
|
+
// Composition wrappers (`allOf` and `anyOf`) followed along one path; deeper wrappers are unclassified.
|
|
59
|
+
const MAX_WRAPPER_HOPS = 16;
|
|
53
60
|
// A `$ref` chain longer than this, such as an alias cycle, is not resolved.
|
|
54
61
|
const MAX_REFERENCE_HOPS = 8;
|
|
55
62
|
// Schema comparisons allowed per tool field; a schema that needs more is unclassified.
|
|
@@ -168,6 +175,178 @@ function compareBounds(inner, outer, codes, reasons) {
|
|
|
168
175
|
}
|
|
169
176
|
}
|
|
170
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* The tighter of a schema's inclusive and exclusive bound on one side, undefined when it has neither,
|
|
180
|
+
* or null when either is not a finite number (such as a draft-04 boolean `exclusiveMinimum`).
|
|
181
|
+
*/
|
|
182
|
+
function numericBound(schema, inclusive, exclusive, lower) {
|
|
183
|
+
if (typeof schema[exclusive] === "boolean") {
|
|
184
|
+
// Draft-04 marks the inclusive keyword itself as exclusive.
|
|
185
|
+
const value = schema[inclusive];
|
|
186
|
+
if (value === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
return isFiniteNumber(value) ? { value, exclusive: schema[exclusive] } : null;
|
|
189
|
+
}
|
|
190
|
+
const candidates = [];
|
|
191
|
+
for (const [keyword, isExclusive] of [[inclusive, false], [exclusive, true]]) {
|
|
192
|
+
const value = schema[keyword];
|
|
193
|
+
if (value === undefined)
|
|
194
|
+
continue;
|
|
195
|
+
if (!isFiniteNumber(value))
|
|
196
|
+
return null;
|
|
197
|
+
candidates.push({ value, exclusive: isExclusive });
|
|
198
|
+
}
|
|
199
|
+
return candidates.reduce((tightest, candidate) => {
|
|
200
|
+
if (tightest === undefined)
|
|
201
|
+
return candidate;
|
|
202
|
+
if (candidate.value === tightest.value)
|
|
203
|
+
return candidate.exclusive ? candidate : tightest;
|
|
204
|
+
return (lower ? candidate.value > tightest.value : candidate.value < tightest.value) ? candidate : tightest;
|
|
205
|
+
}, undefined);
|
|
206
|
+
}
|
|
207
|
+
/** Flags numeric bounds, `multipleOf`, and `uniqueItems` of `outer` that `inner` does not satisfy. */
|
|
208
|
+
function compareNumericAndArrayConstraints(inner, outer, codes, reasons) {
|
|
209
|
+
for (const [inclusive, exclusive, lower] of [["minimum", "exclusiveMinimum", true], ["maximum", "exclusiveMaximum", false]]) {
|
|
210
|
+
const innerBound = numericBound(inner, inclusive, exclusive, lower);
|
|
211
|
+
const outerBound = numericBound(outer, inclusive, exclusive, lower);
|
|
212
|
+
if (innerBound === null || outerBound === null) {
|
|
213
|
+
// A bound that cannot be read still means the same when it did not change.
|
|
214
|
+
const unchanged = Object.is(inner[inclusive], outer[inclusive]) && Object.is(inner[exclusive], outer[exclusive]);
|
|
215
|
+
if (!unchanged)
|
|
216
|
+
reasons.add(codes.unclassified);
|
|
217
|
+
}
|
|
218
|
+
else if (outerBound !== undefined) {
|
|
219
|
+
const covered = innerBound !== undefined && (innerBound.value === outerBound.value
|
|
220
|
+
? innerBound.exclusive || !outerBound.exclusive
|
|
221
|
+
: lower ? innerBound.value > outerBound.value : innerBound.value < outerBound.value);
|
|
222
|
+
if (!covered)
|
|
223
|
+
reasons.add(codes.constraint);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const outerMultiple = outer.multipleOf;
|
|
227
|
+
const innerMultiple = inner.multipleOf;
|
|
228
|
+
const validMultiple = (value) => value === undefined || (isFiniteNumber(value) && value > 0);
|
|
229
|
+
if (!validMultiple(outerMultiple) || !validMultiple(innerMultiple)) {
|
|
230
|
+
if (!Object.is(innerMultiple, outerMultiple))
|
|
231
|
+
reasons.add(codes.unclassified);
|
|
232
|
+
}
|
|
233
|
+
else if (outerMultiple !== undefined) {
|
|
234
|
+
if (innerMultiple === undefined) {
|
|
235
|
+
reasons.add(codes.constraint);
|
|
236
|
+
}
|
|
237
|
+
else if (Number.isInteger(innerMultiple) && Number.isInteger(outerMultiple)) {
|
|
238
|
+
// Every multiple of innerMultiple is a multiple of outerMultiple when outerMultiple divides it.
|
|
239
|
+
if (innerMultiple % outerMultiple !== 0)
|
|
240
|
+
reasons.add(codes.constraint);
|
|
241
|
+
}
|
|
242
|
+
else if (innerMultiple !== outerMultiple) {
|
|
243
|
+
// Validators test fractional multiples with floating-point division, which can reject 0.3
|
|
244
|
+
// under multipleOf 0.1, so a changed fractional multipleOf cannot be judged.
|
|
245
|
+
reasons.add(codes.unclassified);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if ((outer.uniqueItems !== undefined && typeof outer.uniqueItems !== "boolean")
|
|
249
|
+
|| (inner.uniqueItems !== undefined && typeof inner.uniqueItems !== "boolean")) {
|
|
250
|
+
if (!Object.is(inner.uniqueItems, outer.uniqueItems))
|
|
251
|
+
reasons.add(codes.unclassified);
|
|
252
|
+
}
|
|
253
|
+
else if (outer.uniqueItems === true && inner.uniqueItems !== true) {
|
|
254
|
+
reasons.add(codes.constraint);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** The subschema of a schema that is only a one-element `allOf` with documentation, or null. */
|
|
258
|
+
function singleAllOf(schema) {
|
|
259
|
+
if (!isJsonObject(schema) || !Array.isArray(schema.allOf) || schema.allOf.length !== 1)
|
|
260
|
+
return null;
|
|
261
|
+
return Object.keys(schema).every((key) => key === "allOf" || DOCUMENTATION_KEYWORDS.has(key)) ? { inner: schema.allOf[0] } : null;
|
|
262
|
+
}
|
|
263
|
+
/** The alternatives of a schema that is only `anyOf` with documentation, or null. */
|
|
264
|
+
function alternatives(schema) {
|
|
265
|
+
if (!isJsonObject(schema) || !Array.isArray(schema.anyOf))
|
|
266
|
+
return null;
|
|
267
|
+
return Object.keys(schema).every((key) => key === "anyOf" || DOCUMENTATION_KEYWORDS.has(key)) ? schema.anyOf : null;
|
|
268
|
+
}
|
|
269
|
+
function isComposition(schema) {
|
|
270
|
+
return singleAllOf(schema) !== null || alternatives(schema) !== null;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Compares two schemas when at least one is a one-element `allOf` or an `anyOf` with only
|
|
274
|
+
* documentation beside it. Wrappers keep the depth, so their own count bounds how far one path unwraps.
|
|
275
|
+
*/
|
|
276
|
+
function compareComposition(before, after, direction, depth, equal, reasons, scope) {
|
|
277
|
+
if (scope.wrappers >= MAX_WRAPPER_HOPS) {
|
|
278
|
+
reasons.add(CODES[direction].unclassified);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const wrappedBefore = singleAllOf(before);
|
|
282
|
+
const wrappedAfter = singleAllOf(after);
|
|
283
|
+
scope.wrappers += 1;
|
|
284
|
+
if (wrappedBefore !== null || wrappedAfter !== null) {
|
|
285
|
+
// A one-element allOf with only documentation beside it means the same as its subschema.
|
|
286
|
+
compareSchema(wrappedBefore === null ? before : wrappedBefore.inner, wrappedAfter === null ? after : wrappedAfter.inner, direction, depth, equal, reasons, scope);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// A schema that is not an anyOf compares as an anyOf with that schema as its only alternative.
|
|
290
|
+
compareAlternatives(alternatives(before) ?? [before], alternatives(after) ?? [after], direction, depth, equal, reasons, scope);
|
|
291
|
+
}
|
|
292
|
+
scope.wrappers -= 1;
|
|
293
|
+
}
|
|
294
|
+
/** The reference target or declared type that pairs an alternative with its counterpart. */
|
|
295
|
+
function alternativeKey(alternative) {
|
|
296
|
+
if (!isJsonObject(alternative))
|
|
297
|
+
return null;
|
|
298
|
+
if (typeof alternative.$ref === "string")
|
|
299
|
+
return `ref:${alternative.$ref}`;
|
|
300
|
+
return alternative.type === undefined ? null : `type:${JSON.stringify(alternative.type)}`;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Compares `anyOf` alternatives. A value is accepted when any alternative accepts it, so inputs stay
|
|
304
|
+
* compatible when every earlier alternative is covered by a later one, and outputs when every later
|
|
305
|
+
* alternative is covered by an earlier one. Identical alternatives match first, then alternatives with
|
|
306
|
+
* the same reference target or type, then a single remaining alternative on each side.
|
|
307
|
+
*/
|
|
308
|
+
function compareAlternatives(before, after, direction, depth, equal, reasons, scope) {
|
|
309
|
+
const codes = CODES[direction];
|
|
310
|
+
if (before.length > MAX_ALTERNATIVES || after.length > MAX_ALTERNATIVES) {
|
|
311
|
+
reasons.add(codes.unclassified);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const unpairedBefore = [...before];
|
|
315
|
+
const unpairedAfter = [...after];
|
|
316
|
+
for (let index = unpairedBefore.length - 1; index >= 0; index -= 1) {
|
|
317
|
+
const match = unpairedAfter.findIndex((candidate) => sameMeaning(unpairedBefore[index], candidate, equal, scope));
|
|
318
|
+
if (match !== -1) {
|
|
319
|
+
unpairedBefore.splice(index, 1);
|
|
320
|
+
unpairedAfter.splice(match, 1);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const pairs = [];
|
|
324
|
+
for (const key of new Set(unpairedBefore.map(alternativeKey))) {
|
|
325
|
+
if (key === null)
|
|
326
|
+
continue;
|
|
327
|
+
const beforeMatches = unpairedBefore.filter((alternative) => alternativeKey(alternative) === key);
|
|
328
|
+
const afterMatches = unpairedAfter.filter((alternative) => alternativeKey(alternative) === key);
|
|
329
|
+
if (beforeMatches.length !== 1 || afterMatches.length !== 1)
|
|
330
|
+
continue;
|
|
331
|
+
pairs.push([beforeMatches[0], afterMatches[0]]);
|
|
332
|
+
unpairedBefore.splice(unpairedBefore.indexOf(beforeMatches[0]), 1);
|
|
333
|
+
unpairedAfter.splice(unpairedAfter.indexOf(afterMatches[0]), 1);
|
|
334
|
+
}
|
|
335
|
+
if (unpairedBefore.length === 1 && unpairedAfter.length === 1) {
|
|
336
|
+
pairs.push([unpairedBefore.pop(), unpairedAfter.pop()]);
|
|
337
|
+
}
|
|
338
|
+
else if (unpairedBefore.length > 0 && unpairedAfter.length > 0) {
|
|
339
|
+
reasons.add(codes.unclassified);
|
|
340
|
+
}
|
|
341
|
+
for (const [beforeAlternative, afterAlternative] of pairs) {
|
|
342
|
+
compareSchema(beforeAlternative, afterAlternative, direction, depth, equal, reasons, scope);
|
|
343
|
+
}
|
|
344
|
+
// A dropped input alternative may reject values callers send; an added output alternative may
|
|
345
|
+
// produce values consumers do not expect. The opposite changes only narrow or widen safely.
|
|
346
|
+
const narrowedOrWidened = direction === "input" ? unpairedAfter.length === 0 && unpairedBefore.length > 0 : unpairedBefore.length === 0 && unpairedAfter.length > 0;
|
|
347
|
+
if (narrowedOrWidened)
|
|
348
|
+
reasons.add(depth === 0 ? codes.rootType : codes.propertyType);
|
|
349
|
+
}
|
|
171
350
|
/** Flags a `pattern` or `format` that `outer` adds and `inner` lacks; a changed value cannot be compared. */
|
|
172
351
|
function compareStringConstraints(inner, outer, codes, reasons) {
|
|
173
352
|
for (const keyword of STRING_CONSTRAINT_KEYWORDS) {
|
|
@@ -291,22 +470,32 @@ function referenceScope(before, after) {
|
|
|
291
470
|
fragmentsOnly: reachesDefinitionsOnlyThroughFragments(before) && reachesDefinitionsOnlyThroughFragments(after),
|
|
292
471
|
active: new Set(),
|
|
293
472
|
completed: new Map(),
|
|
473
|
+
inProgress: new Map(),
|
|
474
|
+
wrappers: 0,
|
|
294
475
|
same: new Map(),
|
|
295
476
|
pointers: new Map(),
|
|
296
477
|
remaining: MAX_COMPARISONS
|
|
297
478
|
};
|
|
298
479
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
let byAfter = scope.completed.get(before);
|
|
480
|
+
function pairDepths(pairs, before, after) {
|
|
481
|
+
let byAfter = pairs.get(before);
|
|
302
482
|
if (byAfter === undefined)
|
|
303
|
-
|
|
483
|
+
pairs.set(before, byAfter = new Map());
|
|
304
484
|
let depths = byAfter.get(after);
|
|
305
485
|
if (depths === undefined)
|
|
306
486
|
byAfter.set(after, depths = new Set());
|
|
307
|
-
|
|
487
|
+
return depths;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Records a resolved pair as compared at this depth and wrapper count; returns false when it already
|
|
491
|
+
* was. The wrapper count is part of the key because it limits how far that comparison may unwrap.
|
|
492
|
+
*/
|
|
493
|
+
function markCompleted(scope, before, after, depth) {
|
|
494
|
+
const depths = pairDepths(scope.completed, before, after);
|
|
495
|
+
const key = depth * (MAX_WRAPPER_HOPS + 1) + scope.wrappers;
|
|
496
|
+
if (depths.has(key))
|
|
308
497
|
return false;
|
|
309
|
-
depths.add(
|
|
498
|
+
depths.add(key);
|
|
310
499
|
return true;
|
|
311
500
|
}
|
|
312
501
|
/**
|
|
@@ -401,10 +590,18 @@ function compareSchema(before, after, direction, depth, equal, reasons, scope) {
|
|
|
401
590
|
return;
|
|
402
591
|
const resolvedBefore = resolveReferences(before, scope.before);
|
|
403
592
|
const resolvedAfter = resolveReferences(after, scope.after);
|
|
404
|
-
if (resolvedBefore === null || resolvedAfter === null) {
|
|
593
|
+
if (resolvedBefore === null || resolvedAfter === null || depth > MAX_NESTED_DEPTH) {
|
|
405
594
|
reasons.add(codes.unclassified);
|
|
406
595
|
return;
|
|
407
596
|
}
|
|
597
|
+
// When only one side is a composition, the other keeps its reference unresolved, so it can still
|
|
598
|
+
// pair with an alternative that refers to the same definition, as when a model becomes optional.
|
|
599
|
+
const beforeComposed = isComposition(resolvedBefore.schema);
|
|
600
|
+
const afterComposed = isComposition(resolvedAfter.schema);
|
|
601
|
+
if (beforeComposed !== afterComposed) {
|
|
602
|
+
compareComposition(beforeComposed ? resolvedBefore.schema : before, afterComposed ? resolvedAfter.schema : after, direction, depth, equal, reasons, scope);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
408
605
|
if (resolvedBefore.ref !== undefined || resolvedAfter.ref !== undefined) {
|
|
409
606
|
// A reference pair already being compared further up is assumed to hold, so recursive
|
|
410
607
|
// definitions terminate; a pair with one inline side is bounded by the depth limit. A pair
|
|
@@ -412,22 +609,35 @@ function compareSchema(before, after, direction, depth, equal, reasons, scope) {
|
|
|
412
609
|
const pair = resolvedBefore.ref !== undefined && resolvedAfter.ref !== undefined
|
|
413
610
|
? JSON.stringify([resolvedBefore.ref, resolvedAfter.ref])
|
|
414
611
|
: undefined;
|
|
612
|
+
// Reaching a pair again at the same depth, for example through a definition that wraps a
|
|
613
|
+
// reference to itself, is a cycle that never reaches a value, so it cannot be judged.
|
|
614
|
+
const inProgress = pairDepths(scope.inProgress, resolvedBefore.schema, resolvedAfter.schema);
|
|
615
|
+
if (inProgress.has(depth)) {
|
|
616
|
+
reasons.add(codes.unclassified);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
415
619
|
if (pair !== undefined && scope.active.has(pair))
|
|
416
620
|
return;
|
|
417
621
|
if (!markCompleted(scope, resolvedBefore.schema, resolvedAfter.schema, depth))
|
|
418
622
|
return;
|
|
419
623
|
if (pair !== undefined)
|
|
420
624
|
scope.active.add(pair);
|
|
625
|
+
inProgress.add(depth);
|
|
421
626
|
compareSchema(resolvedBefore.schema, resolvedAfter.schema, direction, depth, equal, reasons, scope);
|
|
627
|
+
inProgress.delete(depth);
|
|
422
628
|
if (pair !== undefined)
|
|
423
629
|
scope.active.delete(pair);
|
|
424
630
|
return;
|
|
425
631
|
}
|
|
632
|
+
if (beforeComposed) {
|
|
633
|
+
compareComposition(before, after, direction, depth, equal, reasons, scope);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
426
636
|
const root = depth === 0;
|
|
427
637
|
const structural = scope.fragmentsOnly
|
|
428
638
|
? root ? ROOT_STRUCTURAL_WITH_DEFINITIONS : NESTED_STRUCTURAL_WITH_DEFINITIONS
|
|
429
639
|
: root ? ROOT_STRUCTURAL_KEYWORDS : NESTED_STRUCTURAL_KEYWORDS;
|
|
430
|
-
if (
|
|
640
|
+
if (!isJsonObject(before) || !isJsonObject(after)
|
|
431
641
|
|| !sameMeaning(keywordsOutside(before, structural), keywordsOutside(after, structural), equal, scope)
|
|
432
642
|
|| !equal(before.$schema, after.$schema)) {
|
|
433
643
|
reasons.add(codes.unclassified);
|
|
@@ -446,6 +656,7 @@ function compareSchema(before, after, direction, depth, equal, reasons, scope) {
|
|
|
446
656
|
else if (!enumResult)
|
|
447
657
|
reasons.add(codes.enumChanged);
|
|
448
658
|
compareBounds(inner, outer, codes, reasons);
|
|
659
|
+
compareNumericAndArrayConstraints(inner, outer, codes, reasons);
|
|
449
660
|
compareStringConstraints(inner, outer, codes, reasons);
|
|
450
661
|
if (!sameMeaning(before.items, after.items, equal, scope)) {
|
|
451
662
|
if (unevaluated)
|
package/package.json
CHANGED