Complete implementation of the 3-pass matching algorithm for ambiguous path handling

- Finalized the transition from a duplicate-name blacklist system to a comprehensive 3-pass matching algorithm for both syncback and forward operations, enhancing the handling of ambiguous paths.
- Integrated a new `matching.lua` module to facilitate instance matching during hydration, utilizing unique name matching, class name narrowing, and structural fingerprinting.
- Updated various components, including `ChangeBatcher`, `Reconciler`, and `snapshot` modules, to leverage the new matching logic, ensuring robust file management and improved performance.
- Removed legacy duplicate handling code and streamlined the encoding process for instances with duplicate names.

These changes significantly enhance the clarity and efficiency of file operations, paving the way for improved project management and user experience.
This commit is contained in:
Joe 2026-02-17 02:43:25 -06:00
parent cc77a2cd8a
commit d6162ac210
21 changed files with 2507 additions and 846 deletions

View file

@ -117,9 +117,6 @@ end
-- Forward declaration for recursion
local encodeInstance
-- Track skipped duplicates for logging
local skippedDuplicateCount = 0
-- Encode Attributes if present on any instance
local function encodeAttributes(instance, properties)
-- Try to get Attributes - this works for all instance types
@ -160,18 +157,14 @@ local function encodeTags(instance, properties)
end
encodeInstance = function(instance, parentId, _skipPathCheck)
-- Check if the entire path to this instance is unique (unless we're in a recursive call)
-- For top-level instances, we check the entire ancestor chain for duplicates
-- For children, the parent already verified the path is unique up to that point,
-- so we only need to check siblings at each level (done in the recursion below)
-- Log duplicate-named siblings as a debug message (server handles them via
-- dedup suffixes) but do NOT skip encoding.
if not _skipPathCheck and not isPathUnique(instance) then
skippedDuplicateCount += 1
Log.warn(
"Skipped instance '{}' ({}) - path contains duplicate-named siblings (cannot reliably sync)",
Log.debug(
"Instance '{}' ({}) has duplicate-named siblings in path (server will handle via dedup)",
instance:GetFullName(),
instance.ClassName
)
return nil
end
-- Log encoding at trace level (very detailed)
@ -256,24 +249,12 @@ encodeInstance = function(instance, parentId, _skipPathCheck)
end
end
-- Recursively encode children, skipping those with duplicate names
-- Recursively encode children (including those with duplicate names;
-- the server handles duplicates via dedup suffix system)
local children = {}
local childInstances = instance:GetChildren()
local childDuplicates = findDuplicateNames(childInstances)
for _, child in ipairs(childInstances) do
-- Skip children with duplicate names
if childDuplicates[child.Name] then
skippedDuplicateCount += 1
Log.warn(
"Skipped child instance '{}' ({}) - has duplicate-named siblings (cannot reliably sync)",
child:GetFullName(),
child.ClassName
)
continue
end
-- Pass true to skip duplicate check since we already checked above
local encodedChild = encodeInstance(child, nil, true)
if encodedChild then
table.insert(children, encodedChild)
@ -296,20 +277,6 @@ encodeInstance = function(instance, parentId, _skipPathCheck)
}
end
-- Wrapper function that resets the duplicate count and returns the final count
local function encodeInstanceWithDuplicateTracking(instance, parentId)
skippedDuplicateCount = 0
local result = encodeInstance(instance, parentId, false)
-- Log summary if any duplicates were skipped
if skippedDuplicateCount > 0 then
Log.warn(
"Skipped {} location(s) with duplicate-named siblings during pull (cannot reliably sync)",
skippedDuplicateCount
)
end
return result, skippedDuplicateCount
return function(instance, parentId)
return encodeInstance(instance, parentId, false)
end
return encodeInstanceWithDuplicateTracking