Document and section
docs/IDENTIFIERS.md — §2 Runtime identifiers, the event_id row and the UUIDv7 rationale paragraph.
What is wrong or unclear
Nothing currently written is incorrect. The document says UUIDv7 is "time-sortable, which gives index locality for the append-only journal," and that claim holds. The gap is what it does not say.
UUIDv7 (RFC 9562) encodes a Unix timestamp at millisecond resolution in its leading 48 bits. The remaining 74 bits are random. .NET's Guid.CreateVersion7() does not implement the optional monotonic-counter methods described in RFC 9562 §6.2, so:
- identifiers generated in different milliseconds always sort in creation order;
- identifiers generated in the same millisecond sort in essentially random order relative to each other.
Measured on .NET SDK 10.0.400 (Windows 11 x64) during Stage 0 Wave B toolchain verification: of 200 identifier pairs generated back to back within the same millisecond, 103 sorted in the wrong order — consistent with the ~50% that random low bits predict. Identifiers generated more than one millisecond apart sorted correctly in every trial.
Reproduction:
int outOfOrder = 0;
for (int t = 0; t < 200; t++) {
var x = Guid.CreateVersion7().ToString();
var y = Guid.CreateVersion7().ToString();
if (x.Substring(0, 13) == y.Substring(0, 13) && string.CompareOrdinal(x, y) > 0) outOfOrder++;
}
// observed: 103
Why this matters beyond documentation wording. A reader can reasonably take "time-sortable" to mean event_id provides a total ordering of events. It does not. The append-only journal described in ARCHITECTURE.md ingests capture-hook events in bursts, so many events sharing a millisecond is the normal case rather than an edge case. If Stage 2 orders or replays the journal by sorting on event_id, ordering will be wrong for same-millisecond events roughly half the time, and the resulting defect would be intermittent and expensive to diagnose.
This also bears on idempotency and crash recovery: ARCHITECTURE.md requires that replayed events be idempotent and that a crash between capture and acknowledgement not duplicate memory. Recovery logic that resumes from "the highest event_id seen" would be unsound for the same reason.
Suggested correction
Two changes, both for a later wave — docs/IDENTIFIERS.md is on main, and CONTRIBUTING.md forbids direct commits there.
-
Document the granularity in docs/IDENTIFIERS.md. State that UUIDv7 ordering is guaranteed only across millisecond boundaries, and that identifiers minted within the same millisecond have no defined relative order. Keep the existing index-locality rationale, which is unaffected.
-
Separate identity from ordering in the Stage 2 journal contract. The journal needs its own monotonic sequence number for ordering and replay, with event_id retained for identity and index locality. These are two different jobs and should not share one field.
No change is proposed to the UUIDv7 decision itself. It remains the right choice for the reasons already recorded: a published standard, direct .NET support, and index locality for the journal.
Related
- Discovered during Stage 0 toolchain verification after the .NET 10 SDK was installed, by probing the API that
docs/IDENTIFIERS.md cites rather than assuming its behavior.
- Companion carry-forward from the Wave A Codex review, also awaiting Stage 2/3: the
source_record_id format {adapter}:{native_id} does not specify how to parse a native_id that itself contains a colon.
Document and section
docs/IDENTIFIERS.md— §2 Runtime identifiers, theevent_idrow and the UUIDv7 rationale paragraph.What is wrong or unclear
Nothing currently written is incorrect. The document says UUIDv7 is "time-sortable, which gives index locality for the append-only journal," and that claim holds. The gap is what it does not say.
UUIDv7 (RFC 9562) encodes a Unix timestamp at millisecond resolution in its leading 48 bits. The remaining 74 bits are random. .NET's
Guid.CreateVersion7()does not implement the optional monotonic-counter methods described in RFC 9562 §6.2, so:Measured on .NET SDK 10.0.400 (Windows 11 x64) during Stage 0 Wave B toolchain verification: of 200 identifier pairs generated back to back within the same millisecond, 103 sorted in the wrong order — consistent with the ~50% that random low bits predict. Identifiers generated more than one millisecond apart sorted correctly in every trial.
Reproduction:
Why this matters beyond documentation wording. A reader can reasonably take "time-sortable" to mean
event_idprovides a total ordering of events. It does not. The append-only journal described inARCHITECTURE.mdingests capture-hook events in bursts, so many events sharing a millisecond is the normal case rather than an edge case. If Stage 2 orders or replays the journal by sorting onevent_id, ordering will be wrong for same-millisecond events roughly half the time, and the resulting defect would be intermittent and expensive to diagnose.This also bears on idempotency and crash recovery:
ARCHITECTURE.mdrequires that replayed events be idempotent and that a crash between capture and acknowledgement not duplicate memory. Recovery logic that resumes from "the highestevent_idseen" would be unsound for the same reason.Suggested correction
Two changes, both for a later wave —
docs/IDENTIFIERS.mdis onmain, andCONTRIBUTING.mdforbids direct commits there.Document the granularity in
docs/IDENTIFIERS.md. State that UUIDv7 ordering is guaranteed only across millisecond boundaries, and that identifiers minted within the same millisecond have no defined relative order. Keep the existing index-locality rationale, which is unaffected.Separate identity from ordering in the Stage 2 journal contract. The journal needs its own monotonic sequence number for ordering and replay, with
event_idretained for identity and index locality. These are two different jobs and should not share one field.No change is proposed to the UUIDv7 decision itself. It remains the right choice for the reasons already recorded: a published standard, direct .NET support, and index locality for the journal.
Related
docs/IDENTIFIERS.mdcites rather than assuming its behavior.source_record_idformat{adapter}:{native_id}does not specify how to parse anative_idthat itself contains a colon.