Bulk update: diff generated fields only

planBulkUpdate compared fresh field values with the stored text, so a
peer's "...00Z" created_at (or a web build's millisecond date) read as a
change and got a fresh clock, erasing the created_by node signal; a
record without a priority field got one written. Compare the note's own
generated fields before and after instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWXdsB7CQPNXVN829G1
This commit is contained in:
Krzysztof Rudnicki 2026-11-10 15:41:35 +01:00
parent 757c54beda
commit 4c77dd5d2e
3 changed files with 33 additions and 2 deletions

View File

@ -31,6 +31,10 @@ Log planBulkUpdate(
final note = noteFromRecord(current);
final next = note.copyWith(priority: priority, status: status);
if (next.priority == note.priority && next.status == note.status) continue;
// Generated against generated, never against the stored text: a peer's
// `...00Z` (or a web build's millisecond date) would otherwise read as
// a change and get a fresh clock, erasing the `created_at` node signal.
final before = noteFields(note, hlc);
plan[id] = Record(
id: id,
fields: {
@ -38,7 +42,7 @@ Log planBulkUpdate(
next.copyWith(updatedAt: now),
hlc,
).entries)
if (current.fields[key]?.$1 != value.$1) key: value,
if (before[key]?.$1 != value.$1) key: value,
},
);
}

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.68+1
version: 1.0.69+1
environment:
sdk: ^3.12.2

View File

@ -75,6 +75,33 @@ void main() {
expect((await repo.listNotes()).single.status, Status.done);
});
test(
'a stored value Dart would format differently keeps its clock',
() async {
final (repo, _) = await _open();
final peer = Hlc(wallTimeMs: 1000, counter: 0, nodeId: 'mcp');
// A peer's (or a web build's) timestamp text, and no priority field.
await repo.importLog({
'a': Record(
id: 'a',
fields: {
'text': ('A', peer),
'status': (0, peer),
'created_at': ('2026-01-01T00:00:00Z', peer),
'updated_at': ('2026-01-01T00:00:00Z', peer),
},
),
});
await repo.bulkUpdate({'a'}, status: Status.done);
final fields = repo.exportLog()['a']!.fields;
expect(fields['created_at'], ('2026-01-01T00:00:00Z', peer));
expect(fields.containsKey('priority'), isFalse);
expect(fields['status']!.$2.nodeId, 'dev');
},
);
test('no write when nothing changes or ids are unknown/deleted', () async {
final (repo, p) = await _open();
await repo.upsert(noteFixture('a', 'A'));