Opt-in per-componentKey sub-hash mode (the whole-blob
buildDataFingerprint stays the default). Sub-hashes make the
conflict unit (entity, componentKey): an architect editing placement and
an agent editing Pset_FireSafety on the same wall is not a conflict.
Only components the entity actually carries get a key: a missing pset
has no entry rather than an "empty" hash, so add/remove of a component
is visible as key presence.
Every sub-hash is computed over the same canonical projection
buildDataFingerprint hashes — including the GlobalId-free type
assignment. That is a requirement, not a convenience. diffModels'
content pass uses these sub-hashes as a collision guard: it retires an
added/deleted pair only once they agree, on the stated grounds that a
component hashes a slice of the content dataHash hashes whole, so a
disagreement under an equal dataHash proves a collision. Feeding the
type's GlobalId into type-assignment while dataHash ignores it would
break that: a genuine re-export match would have an equal dataHash and a
differing type-assignment, and the guard would veto the very match the
GlobalId was removed to enable — turning a collision guard into a
re-export filter. It would also make the key-based pass self-contradictory,
reporting an entity as unchanged while listing type-assignment in its
changedComponents. The extra discrimination a GlobalId would buy here is
not worth either. The two functions must project identically — which is why
the direct attributes, Tag included, come from one coreAttributes
call rather than from two object literals held in step by review.
Opt-in per-componentKey sub-hash mode (the whole-blob buildDataFingerprint stays the default). Sub-hashes make the conflict unit (entity, componentKey): an architect editing placement and an agent editing
Pset_FireSafetyon the same wall is not a conflict.Only components the entity actually carries get a key: a missing pset has no entry rather than an "empty" hash, so add/remove of a component is visible as key presence.
Every sub-hash is computed over the same canonical projection buildDataFingerprint hashes — including the GlobalId-free type assignment. That is a requirement, not a convenience.
diffModels' content pass uses these sub-hashes as a collision guard: it retires anadded/deletedpair only once they agree, on the stated grounds that a component hashes a slice of the contentdataHashhashes whole, so a disagreement under an equaldataHashproves a collision. Feeding the type'sGlobalIdintotype-assignmentwhiledataHashignores it would break that: a genuine re-export match would have an equaldataHashand a differingtype-assignment, and the guard would veto the very match the GlobalId was removed to enable — turning a collision guard into a re-export filter. It would also make the key-based pass self-contradictory, reporting an entity asunchangedwhile listingtype-assignmentin itschangedComponents. The extra discrimination a GlobalId would buy here is not worth either. The two functions must project identically — which is why the direct attributes,Tagincluded, come from one coreAttributes call rather than from two object literals held in step by review.