Add Audit Plan for Ambiguous Path Handling

- Introduced a new audit plan detailing the "Ambiguous Path Handling" feature, including three parallel matching algorithm implementations (two in Rust and one in Lua) and their integration with the Reconciler.
- Documented findings related to scoring discrepancies between Lua and Rust implementations, including fixes for stale comments, scoring granularity for Tags/Attributes, and performance improvements in cost computation.
- Enhanced the `trueEquals` function to support fuzzy equality for additional types (UDim, UDim2, NumberRange, Rect, NumberSequence, ColorSequence) with corresponding unit tests to ensure accuracy.

These changes aim to improve the reliability and performance of the matching algorithms while ensuring consistency across implementations.
This commit is contained in:
Joe 2026-02-19 00:02:41 -06:00
parent 7e0538b9f5
commit 69f0b9fd63
4 changed files with 517 additions and 4 deletions

View file

@ -416,9 +416,9 @@ matchChildren = function(
virtualInstances: VirtualInstances,
parentVirtualId: string?,
parentStudioInstance: Instance?,
depth: number?
_depth: number?
): MatchResult
depth = depth or 0
local depth = _depth or 0
if parentVirtualId and parentStudioInstance then
local pc = session.matchCache[parentVirtualId]
if pc then
@ -430,6 +430,7 @@ matchChildren = function(
end
local matched: { MatchPair } = {}
local matchedCosts: { number? } = {}
local matchedV: { [number]: boolean } = {}
local matchedS: { [number]: boolean } = {}
@ -476,6 +477,7 @@ matchChildren = function(
virtualId = virtualChildren[vi],
studioInstance = studioChildren[si],
})
table.insert(matchedCosts, nil)
matchedV[vi] = true
matchedS[si] = true
continue
@ -590,6 +592,7 @@ matchChildren = function(
virtualId = virtualChildren[pair.vi],
studioInstance = studioChildren[pair.si],
})
table.insert(matchedCosts, pair.cost)
matchedV[pair.vi] = true
matchedS[pair.si] = true
end
@ -613,8 +616,13 @@ matchChildren = function(
end
local totalCost = 0
for _, pair in ipairs(matched) do
totalCost += computePairCost(session, pair.virtualId, pair.studioInstance, virtualInstances, math.huge, depth)
for i, pair in ipairs(matched) do
local precomputed = matchedCosts[i]
if precomputed ~= nil then
totalCost += precomputed
else
totalCost += computePairCost(session, pair.virtualId, pair.studioInstance, virtualInstances, math.huge, depth)
end
end
totalCost += (#unmatchedVirtual + #unmatchedStudio) * UNMATCHED_PENALTY

View file

@ -107,6 +107,50 @@ local function trueEquals(a: any, b: any): boolean
if t == "CFrame" then
return cframeEq(a, b)
end
if t == "UDim" then
return fuzzyEq(a.Scale, b.Scale) and a.Offset == b.Offset
end
if t == "UDim2" then
return fuzzyEq(a.X.Scale, b.X.Scale)
and a.X.Offset == b.X.Offset
and fuzzyEq(a.Y.Scale, b.Y.Scale)
and a.Y.Offset == b.Y.Offset
end
if t == "NumberRange" then
return fuzzyEq(a.Min, b.Min) and fuzzyEq(a.Max, b.Max)
end
if t == "Rect" then
return fuzzyEq(a.Min.X, b.Min.X)
and fuzzyEq(a.Min.Y, b.Min.Y)
and fuzzyEq(a.Max.X, b.Max.X)
and fuzzyEq(a.Max.Y, b.Max.Y)
end
if t == "NumberSequence" then
local aKp = a.Keypoints
local bKp = b.Keypoints
if #aKp ~= #bKp then
return false
end
for i = 1, #aKp do
if not (fuzzyEq(aKp[i].Time, bKp[i].Time) and fuzzyEq(aKp[i].Value, bKp[i].Value) and fuzzyEq(aKp[i].Envelope, bKp[i].Envelope)) then
return false
end
end
return true
end
if t == "ColorSequence" then
local aKp = a.Keypoints
local bKp = b.Keypoints
if #aKp ~= #bKp then
return false
end
for i = 1, #aKp do
if not (fuzzyEq(aKp[i].Time, bKp[i].Time) and color3Eq(aKp[i].Value, bKp[i].Value)) then
return false
end
end
return true
end
return false
end

View file

@ -360,6 +360,187 @@ return function()
end)
end)
describe("UDim", function()
it("identical", function()
expect(trueEquals(UDim.new(0.5, 10), UDim.new(0.5, 10))).to.equal(true)
end)
it("scale within epsilon", function()
expect(trueEquals(UDim.new(0.5, 10), UDim.new(0.50005, 10))).to.equal(true)
end)
it("scale beyond epsilon", function()
expect(trueEquals(UDim.new(0.5, 10), UDim.new(0.502, 10))).to.equal(false)
end)
it("offset differs by 1", function()
expect(trueEquals(UDim.new(0.5, 10), UDim.new(0.5, 11))).to.equal(false)
end)
it("zero vs zero", function()
expect(trueEquals(UDim.new(0, 0), UDim.new(0, 0))).to.equal(true)
end)
end)
describe("UDim2", function()
it("identical", function()
expect(trueEquals(UDim2.new(1, 0, 0.5, 100), UDim2.new(1, 0, 0.5, 100))).to.equal(true)
end)
it("X scale within epsilon", function()
expect(trueEquals(UDim2.new(1, 0, 0.5, 100), UDim2.new(1.00005, 0, 0.5, 100))).to.equal(true)
end)
it("X scale beyond epsilon", function()
expect(trueEquals(UDim2.new(1, 0, 0.5, 100), UDim2.new(1.002, 0, 0.5, 100))).to.equal(false)
end)
it("Y scale within epsilon", function()
expect(trueEquals(UDim2.new(1, 0, 0.5, 100), UDim2.new(1, 0, 0.50005, 100))).to.equal(true)
end)
it("X offset differs", function()
expect(trueEquals(UDim2.new(1, 0, 0.5, 100), UDim2.new(1, 1, 0.5, 100))).to.equal(false)
end)
it("Y offset differs", function()
expect(trueEquals(UDim2.new(1, 0, 0.5, 100), UDim2.new(1, 0, 0.5, 101))).to.equal(false)
end)
it("fromOffset identical", function()
expect(trueEquals(UDim2.fromOffset(100, 200), UDim2.fromOffset(100, 200))).to.equal(true)
end)
it("fromScale identical", function()
expect(trueEquals(UDim2.fromScale(0.5, 0.5), UDim2.fromScale(0.5, 0.5))).to.equal(true)
end)
end)
describe("NumberRange", function()
it("identical", function()
expect(trueEquals(NumberRange.new(0, 10), NumberRange.new(0, 10))).to.equal(true)
end)
it("min within epsilon", function()
expect(trueEquals(NumberRange.new(0, 10), NumberRange.new(0.00005, 10))).to.equal(true)
end)
it("max beyond epsilon", function()
expect(trueEquals(NumberRange.new(0, 10), NumberRange.new(0, 10.002))).to.equal(false)
end)
it("single-value range", function()
expect(trueEquals(NumberRange.new(5), NumberRange.new(5))).to.equal(true)
end)
it("different ranges", function()
expect(trueEquals(NumberRange.new(0, 10), NumberRange.new(0, 20))).to.equal(false)
end)
end)
describe("Rect", function()
it("identical", function()
expect(trueEquals(Rect.new(0, 0, 100, 200), Rect.new(0, 0, 100, 200))).to.equal(true)
end)
it("min X within epsilon", function()
expect(trueEquals(Rect.new(0, 0, 100, 200), Rect.new(0.00005, 0, 100, 200))).to.equal(true)
end)
it("max Y beyond epsilon", function()
expect(trueEquals(Rect.new(0, 0, 100, 200), Rect.new(0, 0, 100, 200.5))).to.equal(false)
end)
it("from Vector2 identical", function()
expect(trueEquals(
Rect.new(Vector2.new(10, 20), Vector2.new(30, 40)),
Rect.new(Vector2.new(10, 20), Vector2.new(30, 40))
)).to.equal(true)
end)
end)
describe("NumberSequence", function()
it("identical two-keypoint", function()
expect(trueEquals(
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) })
)).to.equal(true)
end)
it("value within epsilon", function()
expect(trueEquals(
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1.00005) })
)).to.equal(true)
end)
it("value beyond epsilon", function()
expect(trueEquals(
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1.002) })
)).to.equal(false)
end)
it("different keypoint count", function()
expect(trueEquals(
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(0.5, 0.5), NumberSequenceKeypoint.new(1, 1) })
)).to.equal(false)
end)
it("simple constructor identical", function()
expect(trueEquals(NumberSequence.new(5), NumberSequence.new(5))).to.equal(true)
end)
it("envelope within epsilon", function()
expect(trueEquals(
NumberSequence.new({ NumberSequenceKeypoint.new(0, 1, 0.5), NumberSequenceKeypoint.new(1, 1, 0.5) }),
NumberSequence.new({ NumberSequenceKeypoint.new(0, 1, 0.50005), NumberSequenceKeypoint.new(1, 1, 0.5) })
)).to.equal(true)
end)
end)
describe("ColorSequence", function()
it("identical two-keypoint", function()
expect(trueEquals(
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) }),
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) })
)).to.equal(true)
end)
it("color within epsilon", function()
expect(trueEquals(
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) }),
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(0.50005, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) })
)).to.equal(true)
end)
it("color beyond epsilon", function()
expect(trueEquals(
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) }),
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(0.6, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) })
)).to.equal(false)
end)
it("different keypoint count", function()
expect(trueEquals(
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) }),
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(0.5, Color3.new(0, 1, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) })
)).to.equal(false)
end)
it("simple constructor identical", function()
expect(trueEquals(ColorSequence.new(Color3.new(1, 0, 0)), ColorSequence.new(Color3.new(1, 0, 0)))).to.equal(true)
end)
it("time within epsilon", function()
expect(trueEquals(
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) }),
ColorSequence.new({ ColorSequenceKeypoint.new(0.00005, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) })
)).to.equal(true)
end)
end)
describe("type mismatches", function()
it("string vs number", function()
expect(trueEquals("hello", 42)).to.equal(false)