Severity: Critical — silent data corruption on an idiomatic input
Where
src/utils/diff.ts:363-381 (deepClone), gated by isFirestoreOpaque at src/utils/diff.ts:34-43:
export const deepClone = <T>(value: T): T => {
if (value === null || typeof value !== 'object') return value
if (isFirestoreOpaque(value)) return value // requires an .isEqual method
if (Array.isArray(value)) return value.map(deepClone) as T
const result: Record<string, unknown> = {}
for (const key of Object.keys(value)) { // Object.keys(new Date()) === []
result[key] = deepClone((value as Record<string, unknown>)[key])
}
return result as T // → {}
}
isFirestoreOpaque only recognizes objects exposing an .isEqual method (Timestamp, DocumentReference, GeoPoint, Bytes, FieldValue sentinels). A native Date has no .isEqual and its prototype is not Object.prototype, so it is neither opaque nor plain — it falls through to the key walk, and since Date has no own enumerable keys, the clone is {}.
Failure mode
Any Date-valued field in a write payload is replaced by an empty map {}, and that {} is what lands in Firestore. It writes cleanly (an empty map is a legal value), so nothing crashes — the data is just silently wrong.
Trigger — end-to-end path
document.set(data) stores state.localState = deepClone(data) (src/core/document.ts:385), and sync() writes setDoc(docRef, state.localState) (src/core/document.ts:502).
- The update path clones its mutation base at
src/core/document.ts:292; collection paths clone at src/core/collection.ts:287/379/422/482.
Firestore's own SDK accepts native Date (converting it to Timestamp server-side), and a Zod schema field of z.date() invites exactly this input, so set({ createdAt: new Date() }) is idiomatic — and currently writes { createdAt: {} }.
The same defect hits any class instance lacking .isEqual (Map, Set, custom classes), but Date is the realistic, Firestore-supported case.
Verified
Dynamically confirmed against master with vitest:
deepClone({ createdAt: new Date('2026-01-01'), n: 1 })
// → { "createdAt": {}, "n": 1 } (instanceof Date: false, keys: 0)
Suggested fix direction
Handle Date explicitly in deepClone (return new Date(value.getTime()) or pass through by reference — Dates are treated as immutable everywhere else in the pipeline). Audit the sibling helpers (isDeepEqual, valuesEqualForNoOp, computeDiff) for the same blind spot: two Dates currently never compare equal there either (not Object.is-identical, not opaque, not plain objects), which means a Date field also produces spurious diffs on every sync even once the clone bug is fixed.
Filed by the Critical Review agent (automated deep scan). Finding was verified by hand against the cited lines and reproduced dynamically before filing.
Severity: Critical — silent data corruption on an idiomatic input
Where
src/utils/diff.ts:363-381(deepClone), gated byisFirestoreOpaqueatsrc/utils/diff.ts:34-43:isFirestoreOpaqueonly recognizes objects exposing an.isEqualmethod (Timestamp, DocumentReference, GeoPoint, Bytes, FieldValue sentinels). A nativeDatehas no.isEqualand its prototype is notObject.prototype, so it is neither opaque nor plain — it falls through to the key walk, and sinceDatehas no own enumerable keys, the clone is{}.Failure mode
Any
Date-valued field in a write payload is replaced by an empty map{}, and that{}is what lands in Firestore. It writes cleanly (an empty map is a legal value), so nothing crashes — the data is just silently wrong.Trigger — end-to-end path
document.set(data)storesstate.localState = deepClone(data)(src/core/document.ts:385), andsync()writessetDoc(docRef, state.localState)(src/core/document.ts:502).src/core/document.ts:292; collection paths clone atsrc/core/collection.ts:287/379/422/482.Firestore's own SDK accepts native
Date(converting it toTimestampserver-side), and a Zod schema field ofz.date()invites exactly this input, soset({ createdAt: new Date() })is idiomatic — and currently writes{ createdAt: {} }.The same defect hits any class instance lacking
.isEqual(Map,Set, custom classes), butDateis the realistic, Firestore-supported case.Verified
Dynamically confirmed against
masterwith vitest:Suggested fix direction
Handle
Dateexplicitly indeepClone(returnnew Date(value.getTime())or pass through by reference — Dates are treated as immutable everywhere else in the pipeline). Audit the sibling helpers (isDeepEqual,valuesEqualForNoOp,computeDiff) for the same blind spot: two Dates currently never compare equal there either (notObject.is-identical, not opaque, not plain objects), which means a Date field also produces spurious diffs on every sync even once the clone bug is fixed.Filed by the Critical Review agent (automated deep scan). Finding was verified by hand against the cited lines and reproduced dynamically before filing.