Skip to content

Normative: Refactor for atomic Deviations and simpler CompareOptions#15

Open
gibson042 wants to merge 8 commits into
tc39:mainfrom
gibson042:gh-5-followup-2
Open

Normative: Refactor for atomic Deviations and simpler CompareOptions#15
gibson042 wants to merge 8 commits into
tc39:mainfrom
gibson042:gh-5-followup-2

Conversation

@gibson042

@gibson042 gibson042 commented May 19, 2026

Copy link
Copy Markdown
Member

Belated (but as promised) followup to #5.

  • Represent the path as an array rather than a string.
  • Separate option iterate (whether to return an iterator) from mode (whether to inspect property descriptors).
  • Promote an enum-valued option prototypes (whether to match/ignore/recurse [[Prototype]]s).
  • Mention (but do not introduce) other possible configuration options.
  • Use special path segments to represent property descriptor, prototype, and manual getter result arcs.
  • Replace the reason object with a simple kind property describing what differs at path (exhaustively, "extra" for only-in-actual, "missing" for only-in-expected, or "mismatch" for present-in-both but not matching). Deviations are always simple at any given path, with complexity represented instead via distinct paths.

Excludes the SameValueZero → SameValue change, which is in #14 and will likely require conflict resolution one way or the other.

gibson042 added 2 commits May 18, 2026 19:59
Represent the path as an array rather than a string and replace the `reason`
object with a simple `disposition` property.

type/value mismatch can be detected from `actual` and `expected` properties, and
any other deviations should have a distinct path.
Drop `reasons`, separate `iterate` (whether to return an iterator) from `mode`
(whether to inspect property descriptors), and promote an enum-valued
`prototypes` (whether to match/ignore/recurse [[Prototype]]s).

Also mention (but do not introduce) other possible configuration options.
@ljharb

ljharb commented May 19, 2026

Copy link
Copy Markdown
Member

"disposition" isn't a word that evokes clarity imo; separately, i don't want to have to infer type/value mismatch by comparing the values - the point of the api is to do all the comparing for me.

@JakobJingleheimer

Copy link
Copy Markdown
Member

Woh, okee this will take a minute to review—it contains a very large amount of radical changes 😵‍💫

@gibson042

Copy link
Copy Markdown
Member Author

"disposition" isn't a word that evokes clarity imo

Name bikeshedding is welcome, of course. We could also have separate isExtra/isMissing boolean properties.

separately, i don't want to have to infer type/value mismatch by comparing the values - the point of the api is to do all the comparing for me.

Well, the fact of an iteration result already means that it has done the comparing for you, and found a Deviation at some path—either an extra property (disposition "extra"), a missing property (disposition "missing"), or a type/value mismatch between actual and expected (disposition "normal"). But concretely, what more would you expect? A boolean property indicating same-type?

@gibson042

Copy link
Copy Markdown
Member Author

Woh, okee this will take a minute to review—it contains a very large amount of radical changes 😵‍💫

Indeed, which I've hinted at but not written down until now. I just updated the PR description to be a better summary.

@JakobJingleheimer

Copy link
Copy Markdown
Member

Name bikeshedding is welcome, of course. We could also have separate isExtra/isMissing boolean properties.

I mean during Thursday's presentation/defence. Fine to do outside in a dedicated issue/discussion.

disposition

This wouldn't make me think of anything related to this API 😅 disposition to me is someone's mood ("a blithe disposition", "a foul disposition").

Comment thread README.md Outdated
Comment on lines +173 to +179
mode?:
| 'value' // (default) limit to the values of enumerable properties
| 'descriptor' // descend from an object to all of its property
// descriptors rather than its enumerable property values
| 'descriptor-and-value' // like 'descriptor', but also invoke each getter into a value
// associated with the node's path (i.e., as a sibling to the
// descriptor path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems quite a limiting change vs the reasons dict, and forces them to be mutually exclusive (but they aren't inherently so) or cumbersome (like 'descriptor-and-value').

@gibson042 gibson042 May 19, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I contend that they are inherently exclusive—the graph changes from $object --[$key]--> $propertyValue to $object --[$key]--> $descriptor --[{value,writable,get,set,enumerable,configurable}]--> $propertyAttributeValue. And the reason for "descriptor-and-value" is to augment the latter with the results of getter invocation which would otherwise not happen when traversing by descriptors.

Comment thread README.md
Comment on lines +181 to +184
prototypes?:
| 'same-value' // (default) compare the [[Prototype]]s of non-primitive values by SameValue
| 'ignore' // ignore [[Prototype]]
| 'recurse' // structurally compare the [[Prototype]]s of non-primitive values

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, those values for prototype seems fine. But I would merely change it within the reasons dict rather than flattening that—which makes their purpose less clear / more ambiguous (the nesting provides clear scope, specially because it maps reasons input to reasons output).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how reasons could be extended to accommodate this, especially to expose what the [[Prototype]] on each side actually is.

@gibson042 gibson042 May 19, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, consider the deviations from comparing an expected empty array of realm A to an actual empty array of realm B in which Array.prototype has been extended with a new enumerable method "smoosh".

  • "same-value"
    1. { path: [{ special: "prototype" }], expected: %A.Array.prototype%, actual: %B.Array.prototype%, disposition: "normal" }
  • "ignore"
    1. [no deviations]
  • "recurse"
    1. { path: [{ special: "prototype" }, "smoosh"], expected: undefined, actual: <function>, disposition: "extra" }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how reasons could be extended to accommodate this

exactly as you did here, just nested in reason

reasons: {
  
  prototypes?:
    | 'same-value' // (default) compare the [[Prototype]]s of non-primitive values by SameValue
    | 'ignore'     // ignore [[Prototype]]
    | 'recurse'    // structurally compare the [[Prototype]]s of non-primitive values
  
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is dropping reasons from both input and output... what would you expect compare(arrFromA, arrFromB, { reasons: { prototypes: "recurse" } }) to produce?

Comment thread README.md
type CompareOptions = {
mode?:
| 'fast' // (default) return => boolean
iterate?:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super opposed—the new name does more concretely describe what it does, but it also narrows its potential. I originally chose "mode" because it was not narrow (meaning extensible without a breaking change).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mode just seemed like a better name for the option that differentiates between value traversal vs. descriptor traversal. I'm open to bikeshedding here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might there be more modes in the future? e.g. 'text' that produces what inspect does below?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hildjj hmm? inspect is a different proposal for a different API. It would work something like

inspect(compare(a, b)) →

[
	{
		foo: [
			…
+			'chocolate',
-			'strawberry',
		],
		bar: [
			…
+			'vanilla',
		],
	},
]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can certainly imagine a third kind of return shape, which would be controlled by this parameter. Maybe it should be returns?: "has-deviations" | "iterate-deviations"? Or maybe we should remove this option in favor of separate functions, e.g. deviates: <T>(expected: T, actual: T, options: CompareOptions): (true | undefined) and compare: <T>(expected: T, actual: T, options: CompareOptions): (IterableIterator<Deviation> | undefined). Again, bikeshedding welcome.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bikeshed: "diff" might be better than "deviations".

Re inspect, yes, I get that it's a different proposal, but it would be nice if the code for that in this case was trivial because the formatted bits were easily available when needed, since it would be easier to produce that diff text while doing the diff than it would be to reconstruct it later.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bikeshed: "diff" might be better than "deviations".

we considered that but specifically avoided diff because it's already loaded. diff is what inspect produces; deviations is information about differences.

Re inspect, yes, I get that it's a different proposal, but it would be nice if the code for that in this case was trivial because the formatted bits were easily available when needed, since it would be easier to produce that diff text while doing the diff than it would be to reconstruct it later.

are you proposing combining that into this? if so, that's a no-go: the committee already insisted they be separated.

Comment thread README.md Outdated
},
>;
type Deviations = IterableIterator<{
path: Array<string | symbol | { special: "descriptor" | "prototype" }>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. I was speaking with Ruben this morning about a related interop problem: when inspect subsequently consumes Deviations, it doesn't have sufficient info to reconstruct the input:

Expected:

[
	{
		foo: ['vanilla', 'strawberry'],
		bar: ['chocolate', 'strawberry', 'vanilla'],
	},
]

Actual:

[
	{
		foo: ['vanilla', 'chocolate'],
		bar: ['chocolate', 'strawberry'],
	},
]

Deviations:

[
	['0.foo.1', {
		actual: 'chocolate',
		expected: 'strawberry',
		reason: {
			equality: true,
		},
	}],
	['0.bar.2', {
		actual: undefined,
		expected: 'vanilla',
		reason: {
			missing: true,
		},
	}],
]

Inspect:

[
	{
		foo: [
			…
+			'chocolate',
-			'strawberry',
		],
		bar: [
			…
+			'vanilla',
		],
	},
]

Inspect doesn't concretely know '0' represents an array (it could be an object with a '0', and 'foo' could be on a RegExp instance).

I was thinking of adjusting path to something like what you've done here but to provide that reconstruction info.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconstruction in general is only possible if you have all nodes rather than just those that deviate, e.g. actual { id: 1 } vs. expected { id: 0 } is distinct from actual { __proto__: null, id: 1 } vs. expected { __proto__: null, id: 0 } but their deviations are indistinguishable.

But regardless, it's universally more useful to represent the path structure as an array rather than expecting consumers to parse strings (which they'll invariably screw up when property names contain punctuation that appears elsewhere in the string, such as " / ( / ) / . / [ / ]), and in fact the string form is incapable of distinguishing properties keyed by distinct symbols with the same description (e.g., in { [Symbol("foo")]: 1, [Symbol("foo")]: 2 }).

@JakobJingleheimer JakobJingleheimer May 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconstruction in general is only possible if you have all nodes rather than just those that deviate

Why? I think that's not true/necessary.

e.g. actual { id: 1 } vs. expected { id: 0 } is distinct from actual { __proto__: null, id: 1 } vs. expected { __proto__: null, id: 0 } but their deviations are indistinguishable.

We only care about that when reason.prototype is enabled, in which case the parent of id would deviate (reason: 'prototype') if one had a null prototype and the other didn't, and then the value of id also deviates (reason: 'equality').

But regardless, it's universally more useful to represent the path structure as an array rather than expecting consumers to parse strings (which they'll invariably screw up when property names contain punctuation that appears elsewhere in the string, such as " / ( / ) / . / [ / ]), and in fact the string form is incapable of distinguishing properties keyed by distinct symbols with the same description (e.g., in { [Symbol("foo")]: 1, [Symbol("foo")]: 2 }).

👍

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconstruction in general is only possible if you have all nodes rather than just those that deviate

Why? I think that's not true/necessary.

e.g. actual { id: 1 } vs. expected { id: 0 } is distinct from actual { __proto__: null, id: 1 } vs. expected { __proto__: null, id: 0 } but their deviations are indistinguishable.

We only care about that when reason.prototype is enabled, in which case the parent of id would deviate (reason: 'prototype') if one had a null prototype and the other didn't, and then the value of id also deviates (reason: 'equality').

Here's another example: actual { id: 1, label: "foo" } vs. expected { id: 1, label: "bar" } is distinct from actual { label: "foo" } vs. expected { label: "bar" } but their deviations are indistinguishable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What?? The deviations are not indistinguishable at all: [label] => { actual: 'foo', expected: 'bar' reason: 'equality' }

@gibson042 gibson042 May 21, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that's the deviation for both { id: 1, label: "foo" } vs. expected { id: 1, label: "bar" } and actual { label: "foo" } vs. expected { label: "bar" }. So in what sense are those deviations distinguishable?

@ljharb

ljharb commented May 19, 2026

Copy link
Copy Markdown
Member

What I'd expect is the reason for the deviation :-) iow, either a human or machine readable way to explain why it was considered a deviation.

@gibson042

gibson042 commented May 19, 2026

Copy link
Copy Markdown
Member Author

What I'd expect is the reason for the deviation :-) iow, either a human or machine readable way to explain why it was considered a deviation.

Would that be satisfied by renaming disposition to reason and its "normal" value to e.g. "mismatch"?

  • "extra" ⇒ present in actual but not expected
  • "missing" ⇒ present in expected but not actual
  • "normal" "mismatch" ⇒ present in both but not matching

@ljharb

ljharb commented May 19, 2026

Copy link
Copy Markdown
Member

That's slightly better than a single boolean, but since the implementation has all the information about exactly why it's different, why not encode that so I don't have to replicate internal logic?

@hildjj

hildjj commented May 19, 2026

Copy link
Copy Markdown

What I'd expect is the reason for the deviation :-) iow, either a human or machine readable way to explain why it was considered a deviation.

Not human-readable, please. I18n considerations, the inevitable desire to parse it, etc.

@ljharb

ljharb commented May 19, 2026

Copy link
Copy Markdown
Member

Fair enough! I'm also happy with lots of machine-readable info that i can use to construct my own english message.

@gibson042

gibson042 commented May 19, 2026

Copy link
Copy Markdown
Member Author

That's slightly better than a single boolean, but since the implementation has all the information about exactly why it's different, why not encode that so I don't have to replicate internal logic?

In what way does reason "mismatch" fail to communicate exactly why the values at a particular path are different? What internal logic do you think you would have to replicate, and what modification to the Deviation of this PR would save you the need to do so? Some concrete examples would help, if you can provide them.

@gibson042

Copy link
Copy Markdown
Member Author

The latest commits rename disposition: "extra" | "missing" | "normal" into kind: "extra" | "missing" | "mismatch".

@ljharb

ljharb commented May 20, 2026

Copy link
Copy Markdown
Member

"mismatch" doesn't tell me in what way it's a mismatch. is the type different, is the value different? if it's an object, are its property names different, or its property values under a given key?

It's possible that what you have already is granular enough to cover it; but it's tough to review this pre-merge, so i may need to wait til after to list concrete examples.

@gibson042

Copy link
Copy Markdown
Member Author

"mismatch" doesn't tell me in what way it's a mismatch. is the type different, is the value different?

Different type implies different value, so the latter covers the former. And "type" on its own seems too coarse to specifically promote when any consumer who actually wants that can use typeof deviation.actual and typeof deviation.expected.

if it's an object, are its property names different, or its property values under a given key?

If property names or values differ, then path will extend beyond the object to the property that differs and kind will differentiate the two ("extra" or "missing" for the former, "mismatch" for the latter)... see example Iterated: multiple leafs unequal and missing:

compare(
  { foo: { bar: 'a'           } },
  { foo: { bar: 'b', qux: 'c' } },
  { iterate: 'deviations' },
);

Iterator => IterableIterator(2) {
  {
    path: ['foo', 'bar'],
    expected: 'a',
    actual: 'b',
    kind: 'mismatch',
  },
  {
    path: ['foo', 'qux'],
    expected: undefined,
    actual: 'c',
    kind: 'extra',
  },
}

It's possible that what you have already is granular enough to cover it; but it's tough to review this pre-merge, so i may need to wait til after to list concrete examples.

Here's what it looks like now: https://github.com/gibson042/tc39-proposal-comparisons/blob/gh-5-followup-2/README.md

Comment thread README.md Outdated
* TypedArrays containing the _same values in the same sequence_ are equal, except when `CompareOptions.reasons.constructor` is enabled.
* A box primitive (eg `new Boolean(true)`) equals its primitive (eg `true`), except when `CompareOptions.reasons.constructor` is enabled.
* TypedArrays containing the _same values in the same sequence_ are equal, except when not ignoring prototypes.
* A boxed primitive (eg `new Boolean(true)`) never equals an actual primitive (eg `true`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 for the "never"

Not necessarily opposed to collapsing constructor into prototype, but that seems like conflating things that have separate value. What's your rationale for it?

@gibson042 gibson042 May 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 for the "never"

Reworded to be less absolute.

Not necessarily opposed to collapsing constructor into prototype, but that seems like conflating things that have separate value. What's your rationale for it?

What do you mean by "collapsing constructor into prototype"? This PR has no special treatment for constructor, which in most cases is an own property on the [[Prototype]] and would therefore be exposed directly only by prototypes: "recurse" and indirectly by prototypes: "same-value"... it removes conflation of separate nodes in the reference graph.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "collapsing constructor into prototype"? This PR has no special treatment for constructor

it does: https://github.com/JakobJingleheimer/proposal-comparisons/blob/83f44794269a6506cd3e41d1e6ed2cbed4b4b2ba/README.md?plain=1#L196-L197

reasons.constructor false | true
Whether to consider constructor. This affects, amongst others, Box Primitives (new Boolean(true) vs true) and TypedArrays new Int8Array([1,2]) vs new Uint8Array([1,2])) where differenes are pedantic.

and

https://github.com/JakobJingleheimer/proposal-comparisons/blob/83f44794269a6506cd3e41d1e6ed2cbed4b4b2ba/README.md?plain=1#L274-L279

Equality

[…]

  • TypedArrays containing the same values in the same sequence are equal, except when CompareOptions.reasons.constructor is enabled.
  • A box primitive (eg new Boolean(true)) equals its primitive (eg true), except when CompareOptions.reasons.constructor is enabled.

@gibson042 gibson042 May 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "collapsing constructor into prototype"? This PR has no special treatment for constructor

it does:

https://github.com/JakobJingleheimer/proposal-comparisons/blob/83f44794269a6506cd3e41d1e6ed2cbed4b4b2ba/README.md?plain=1#L196-L197

No, this PR has no special treatment for constructor: https://github.com/gibson042/tc39-proposal-comparisons/blob/gh-5-followup-2/README.md

The only remaining mention is "CompareOptions might also be extended… Treat the constructor of a non-primitive value as a child for either leaf comparison or recursion, even when ignoring [[Prototype]]" (which would likely be reintroduced if we consider the above boxed primitive use cases sufficiently important and/or common).

Comment thread README.md
path: Array<string | symbol | { special: "descriptor" | "prototype" | "value" }>,
actual: unknown,
expected: unknown,
kind: "extra" | "missing" | "mismatch",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind (enum) instead of reasons (dict) would have a perf benefit that I think @o- would like. But do we lose valuable information? I think more than 1 reason would never happen—I think we said the engine gets to pick whichever is most relevant? (following "general to specific")

If we do this, I think reason is a better name than kind (which is very ambiguous), using the existing keys from reasons (constructor, enumerability, equality, prototype, type, etc).

@gibson042 gibson042 May 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be misunderstanding the essence of this change... fundamentally, it is not even possible to have more than 1 reason at the same path. Differing prototypes is a Deviation at path [{ special: "prototype" }], differing constructors is a Deviation at path [{ special: "prototype" }, "constructor"], differing enumerabilities is a mismatch Deviation at path [propertyKey, { special: "descriptor" }, "enumerable"], etc.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be misunderstanding the essence of this change... fundamentally, it is not even possible to have more than 1 reason at the same path

What does "at the same path" mean? There could be multiple deviations, but maybe only if somewhere in a branch prototype is different (and prototype differentiation is enabled).

I don't understand why you would put deviation info in the path list. That seems conflating (and also confusing AF).

@gibson042 gibson042 May 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not putting deviation info in the path list, I'm making Deviation atomic and scoping each instance to a path.

Comment thread README.md
Comment thread README.md
Comment on lines -170 to +177
| 'fast' // (default) return => boolean
| 'full' // return => Iterator<Deviation>
| 'value' // (default) limit to the values of enumerable properties
| 'descriptor' // descend from an object to all of its property
// descriptors rather than its enumerable property values
| 'descriptor-and-value' // like 'descriptor', but also invoke each getter into a value

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is much too limiting and also creates multiple sources of truth for what it's doing, which may end up conflicting.

@gibson042 gibson042 May 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that without this, relevant differences can be undetectable. Consider the following:

let lastValue = 0n;
const counter = () => ++lastValue;
const a = Object.defineProperty({}, "counter", { enumerable: true, get: counter });
const b = Object.defineProperty({}, "counter", { enumerable: true, get: counter });

compare(a, b, { iterate: "deviations" });
// => Deviation { path: ["counter"], expected: 1n, actual: 2n, kind: "mismatch" }

compare(a, b, { iterate: "deviations", mode: "descriptor" });
// => no Deviations

compare(a, b, { iterate: "deviations", mode: "descriptor-and-value" });
// => Deviation { path: ["counter", { special: "value" }], expected: 1n, actual: 2n, kind: "mismatch" }

Switching to use descriptors hides the fact that the referentially-equal property getter returns different values, which is fine in many cases but in others should be considered as constituting a deviation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, okay. I understand what you're getting at. This is already possible in the current though. Why is this better (or why is the current worse)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, okay. I understand what you're getting at. This is already possible in the current though.

I don't see anything in the current README that seems to cover configurability of whether to consider a property value in addition to property descriptor attributes. Can you provide a concrete example to demonstrate what I'm missing?

Why is this better (or why is the current worse)?

I don't think I can answer that narrowly without seeing an answer to the above request, but what makes this overall PR better is that atomic Deviations with reason being exactly one of extra vs. missing vs. mismatch are simpler to consume and comprehend than complex/polymorphic Deviations.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CompareOptions.reasons.descriptors enable it to consider descriptors or disable it to consider values

@gibson042 gibson042 Jun 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concretely, how can the proposed CompareOptions.reasons.descriptors be used to switch between realizing a and b as deviating vs. not in the above example, and with what output?

let lastValue = 0n;
const counter = () => ++lastValue;
const a = Object.defineProperty({}, "counter", { enumerable: true, get: counter });
const b = Object.defineProperty({}, "counter", { enumerable: true, get: counter });

I see three cases (only [[Get]] result [value] vs. only [[GetOwnProperty]] result [property descriptor] vs. both), and am interested to understand how a single boolean option can accommodate all of them.

@JakobJingleheimer

Copy link
Copy Markdown
Member

or its property values under a given key?

key is the identifier, a value is happenstance, so the key is what matters for determining what to pair for the comparison.

@gibson042 gibson042 changed the title Normative: Refactor Deviations and simplify CompareOptions Normative: Refactor for atomic Deviations and simpler CompareOptions May 21, 2026
@gibson042

Copy link
Copy Markdown
Member Author

We discussed this synchronously, and I have some homework...

  • wait for doc: update Deviation's key (string) → path (array) #19 to land, and then rebase on top of it (reducing the scope of changes)
  • don't rename "mode" to "iterate". Instead, name the new option "consider" (and anticipate a future rename)
  • add an example demonstrating descriptor differences such as expected { foo: "bar" } vs. actual { get foo() { return "bar"; } } (deviations { path: ["foo", { special: "descriptor" }, "value"], expected: "bar", actual: undefined, kind: "mismatch" } and { path: ["foo", { special: "descriptor" }, "get"], expected: undefined, actual: <function "get foo" () {…}>, kind: "mismatch" })
  • possibly also include a diagram showing how "value" vs. "descriptor" vs. "descriptor-and-value" changes the shape of the compared graphs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants