From 1279cae0ce0aae54d06ee1bfdd2f3631a3ec6355 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 11 Oct 2026 00:11:26 -0400 Subject: [PATCH 1/4] fix(trogonstack-protobuf): stop teaching provenance as payload fields so callers cannot claim another identity Signed-off-by: Yordis Prieto --- .../skills/protobuf-design-messages/SKILL.md | 2 +- .../references/event-sourced-contracts.md | 43 ++++++++++++++++--- .../references/example-project-schema.md | 25 +++++------ .../references/review-checklist.md | 1 + .../references/units-money-and-time.md | 2 + 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md index 482026b..1391fa2 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md @@ -22,7 +22,7 @@ Run `buf format -w` and `buf lint` (with `STANDARD`, buf's default) before anyth 2. **No primitive obsession.** Identifiers, references, and out-of-line content pointers are value objects, not bare strings; units, money, and instants follow the ecosystem's naming patterns. 3. **Enums encode an exhaustive choice, not a growable boolean.** The zero value is always `_UNSPECIFIED`, code must reject it, and a boolean that might need a third state should be an enum instead. 4. **Know what the schema can and cannot guarantee.** Editions presence and `protovalidate` constraints are schema-level tools with real limits; some invariants need live state and belong in the service. -5. **Event-sourced contracts separate commands, events, state, and faults.** Commands are imperative, events are past tense, and an aggregate never gets an event for a fact it does not own. +5. **Event-sourced contracts separate commands, events, state, and faults.** Commands are imperative, events are past tense, who and when live in the event envelope rather than the payload, and an aggregate never gets an event for a fact it does not own. 6. **Rename early when words collide with product vocabulary.** The cheapest moment to fix a name is before anything generates code from it. 7. **A file is named after what it defines.** One top-level definition per file in `lower_snake_case`, a service in `_service.proto` named `Service`, and its own `Request` and `Response` for every RPC. diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md index 09a3b60..7e8ac40 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md @@ -5,6 +5,7 @@ Use this reference when designing commands, events, state, or faults for an even ## Contents - Name by Tense +- Who and When Belong to the Envelope, Not the Payload - Record Creation-Time Facts on the Creation Event, Even When Another System Owns What Happens Later - Do Not Add an Event for a Fact the Aggregate Does Not Own - State and Faults @@ -27,18 +28,49 @@ message ProjectRenamed { ... } message ProjectArchived { ... } ``` +## Who and When Belong to the Envelope, Not the Payload + +Every event store already records provenance for each event: the principal whose command produced it, and when it was appended. Keep that in the event's envelope (metadata or headers, written by the runtime), never as payload fields. A `created_by`, `requested_by`, `creator`, `principal_id`, `created_at`, or `occurred_at` on a command or event duplicates the envelope at best, and at worst lets the caller decide whose name goes on the record. + +Commands are where this goes wrong first. The issuing principal comes from authentication, never from a field the caller fills in; a `creator` on `CreateProject` lets anyone claim to be anyone. Copying it onto the event then turns that claim into a recorded fact. + +```protobuf +// Avoid: provenance copied into the payload, and on the command, supplied by the caller. +message CreateProject { + string name = 1; + UserId created_by = 2; +} + +message ProjectArchived { + ProjectId project_id = 1; + UserId archived_by = 2; + google.protobuf.Timestamp archived_at = 3; +} + +// Prefer: the payload carries only domain facts; who and when come from the envelope. +message CreateProject { + string name = 1; +} + +message ProjectArchived { + ProjectId project_id = 1; +} +``` + +Consumers that need the creator (an access-control system granting the creator membership, an audit view) read it from the envelope. A read model or query response may surface it as a field, because there it is a projection of the envelope, not a second source of truth. + +An actor or time field belongs in the payload only when it is a domain fact distinct from provenance: an approver named in a command that a system process issues on their behalf, a due date, the time an external occurrence happened as opposed to when it was recorded. If the value is always the issuing principal or the append time, it is provenance. No linter catches this; it has to be checked in review. + ## Record Creation-Time Facts on the Creation Event, Even When Another System Owns What Happens Later -A resource's creation event is the one place certain facts can ever be recorded as "true at the moment this came into existence": who created it, where it was placed at birth, what access level it started with. Even if another system owns moves, renames, or access changes afterward, the creation event still owns the initial values, because nothing else will ever again be in a position to say what they were at creation time. +A resource's creation event is the one place certain facts can ever be recorded as "true at the moment this came into existence": where it was placed at birth, what access level it started with. (Who created it and when are provenance, recorded in the envelope as above.) Even if another system owns moves, renames, or access changes afterward, the creation event still owns the initial values, because nothing else will ever again be in a position to say what they were at creation time. ```protobuf message ProjectCreated { ProjectId project_id = 1; string name = 2; - UserId created_by = 3; - NodeId parent = 4; // where it was placed at birth - AccessPreset initial_access_preset = 5; // access choice at creation, even if access changes later move elsewhere - google.protobuf.Timestamp created_at = 6; + NodeId parent = 3; // where it was placed at birth + AccessPreset initial_access_preset = 4; // access choice at creation, even if access changes later move elsewhere } ``` @@ -115,6 +147,7 @@ ProjectId project_id = 1; ## Review Questions - Are commands named imperatively and events named in the past tense? +- Does any command or event carry who issued it or when it happened (`*_by`, `creator`, `principal_id`, `created_at`, `occurred_at`) instead of leaving that to the envelope, and does any command let the caller supply its own identity? - Does the creation event carry every fact that is only knowable at creation time, even if later changes to those facts are owned elsewhere? - Does any event represent a fact the aggregate does not own, duplicating another system's source of truth? - Are faults modeled as a `oneof` of specific types rather than a generic error string or code? diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/example-project-schema.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/example-project-schema.md index aefb94f..659cd63 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/example-project-schema.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/example-project-schema.md @@ -15,7 +15,6 @@ edition = "2023"; package example.projects.v1alpha1; import "buf/validate/validate.proto"; -import "google/protobuf/timestamp.proto"; // --- Identifiers and references (principle 2: no primitive obsession) --- @@ -23,10 +22,6 @@ message ProjectId { string value = 1 [(buf.validate.field).string.min_len = 1]; } -message UserId { - string value = 1 [(buf.validate.field).string.min_len = 1]; -} - // A hierarchy position. Bare `parent` names a role (principle 1's exception): // a position can point at a node of any kind, which is the point of the field. message NodeId { @@ -76,8 +71,10 @@ message CreateProject { string name = 1 [(buf.validate.field).string.min_len = 1]; NodeId parent = 2; AccessPreset access_preset = 3 [ - (buf.validate.field).enum.defined_only = true, - (buf.validate.field).enum = {not_in: [0]} + (buf.validate.field).enum = { + defined_only: true + not_in: [0] + } ]; Money budget = 4; } @@ -96,24 +93,21 @@ message ArchiveProject { message ProjectCreated { ProjectId project_id = 1; string name = 2; - UserId created_by = 3; // Placement and access at birth. Later moves and access changes are owned // by other systems; this event only records the facts true at creation. - NodeId parent = 4; - AccessPreset initial_access_preset = 5; - Money initial_budget = 6; - google.protobuf.Timestamp created_at = 7; + // Who created it and when live in the event envelope, not here. + NodeId parent = 3; + AccessPreset initial_access_preset = 4; + Money initial_budget = 5; } message ProjectRenamed { ProjectId project_id = 1; string name = 2; - google.protobuf.Timestamp renamed_at = 3; } message ProjectArchived { ProjectId project_id = 1; - google.protobuf.Timestamp archived_at = 2; } // Deliberately absent: ProjectMoved, ProjectAccessChanged. Placement and @@ -147,12 +141,13 @@ message RenameProjectFault { | Choice | Principle | |---|---| -| `ProjectId`, `UserId`, `NodeId` as wrapper messages | No primitive obsession | +| `ProjectId`, `NodeId` as wrapper messages | No primitive obsession | | `NodeId parent` bare, no `_id` suffix | `parent` names hierarchy position, never qualified | | `Money` with `amount_micros` + `currency_code` | Units and currency travel together, never a bare float; the schema checks the code's shape, the service checks it is a real ISO 4217 code | | `AccessPreset` zero value rejected via `buf.validate.field.enum` | An omitted choice cannot silently widen access | | `CreateProject.name` marked `required` via `string.min_len = 1` | Schema-checkable invariant expressed portably | | `ProjectCreated` carries `parent` and `initial_access_preset` | Creation-time facts recorded even though later changes are owned elsewhere | +| No `created_by`, `created_at`, or `archived_at` on commands or events | Who and when are provenance, recorded in the event envelope | | `Project` state omits `parent` and access | Folded state holds only facts the aggregate's own events keep current | | No `ProjectMoved` or `ProjectAccessChanged` event | The aggregate does not emit events for facts it does not own | | `RenameProjectFault` as a `oneof` | Specific, matchable fault types instead of a generic error | diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md index b0dc008..1d8a0fa 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md @@ -46,6 +46,7 @@ Each remaining item is a symptom of a principle `buf lint` cannot check, not a r Skip this section for plain CRUD/RPC schemas. - [ ] Commands are imperative; events are past tense +- [ ] No command or event carries provenance (who issued it, when it happened); the envelope does, and no command lets the caller supply its own identity - [ ] The creation event carries every fact only knowable at creation time - [ ] No event duplicates a fact owned by another system - [ ] Faults are a `oneof` of specific types, not a generic error string diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md index f8fb67d..03179b5 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md @@ -47,6 +47,8 @@ int64 created = 1; google.protobuf.Timestamp created_at = 1; ``` +On an event-sourced command or event, the time it happened is provenance and belongs in the envelope, not a payload `_at` field; see [event-sourced-contracts.md](event-sourced-contracts.md). + ## Review Questions - Does any numeric field omit its unit from the name? From 620c527f4da7295f5aee4fb2c3ab6c7ed1db9d5f Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 11 Oct 2026 00:21:18 -0400 Subject: [PATCH 2/4] fix(trogonstack-protobuf): tie envelope provenance to infrastructure guarantees and separate domain time Signed-off-by: Yordis Prieto --- .../skills/protobuf-design-messages/SKILL.md | 2 +- .../references/event-sourced-contracts.md | 30 ++++++++++++++++--- .../references/example-project-schema.md | 2 +- .../references/review-checklist.md | 4 ++- .../references/units-money-and-time.md | 2 +- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md index 1391fa2..680a854 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md @@ -22,7 +22,7 @@ Run `buf format -w` and `buf lint` (with `STANDARD`, buf's default) before anyth 2. **No primitive obsession.** Identifiers, references, and out-of-line content pointers are value objects, not bare strings; units, money, and instants follow the ecosystem's naming patterns. 3. **Enums encode an exhaustive choice, not a growable boolean.** The zero value is always `_UNSPECIFIED`, code must reject it, and a boolean that might need a third state should be an enum instead. 4. **Know what the schema can and cannot guarantee.** Editions presence and `protovalidate` constraints are schema-level tools with real limits; some invariants need live state and belong in the service. -5. **Event-sourced contracts separate commands, events, state, and faults.** Commands are imperative, events are past tense, who and when live in the event envelope rather than the payload, and an aggregate never gets an event for a fact it does not own. +5. **Event-sourced contracts separate commands, events, state, and faults.** Commands are imperative, events are past tense, provenance (who issued it, when it was recorded) lives in an envelope the infrastructure guarantees, domain time stays in the payload, and an aggregate never gets an event for a fact it does not own. 6. **Rename early when words collide with product vocabulary.** The cheapest moment to fix a name is before anything generates code from it. 7. **A file is named after what it defines.** One top-level definition per file in `lower_snake_case`, a service in `_service.proto` named `Service`, and its own `Request` and `Response` for every RPC. diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md index 7e8ac40..e891924 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md @@ -30,7 +30,7 @@ message ProjectArchived { ... } ## Who and When Belong to the Envelope, Not the Payload -Every event store already records provenance for each event: the principal whose command produced it, and when it was appended. Keep that in the event's envelope (metadata or headers, written by the runtime), never as payload fields. A `created_by`, `requested_by`, `creator`, `principal_id`, `created_at`, or `occurred_at` on a command or event duplicates the envelope at best, and at worst lets the caller decide whose name goes on the record. +Who issued a command and when its events were recorded are provenance. When the infrastructure records them, keep them in the event's envelope (metadata or headers) and out of the payload. A `created_by`, `requested_by`, `creator`, `principal_id`, or `recorded_at` on a command or event then duplicates the envelope at best, and at worst lets the caller decide whose name goes on the record. Commands are where this goes wrong first. The issuing principal comes from authentication, never from a field the caller fills in; a `creator` on `CreateProject` lets anyone claim to be anyone. Copying it onto the event then turns that claim into a recorded fact. @@ -57,9 +57,29 @@ message ProjectArchived { } ``` -Consumers that need the creator (an access-control system granting the creator membership, an audit view) read it from the envelope. A read model or query response may surface it as a field, because there it is a projection of the envelope, not a second source of truth. +### The Infrastructure Has to Make It True -An actor or time field belongs in the payload only when it is a domain fact distinct from provenance: an approver named in a command that a system process issues on their behalf, a due date, the time an external occurrence happened as opposed to when it was recorded. If the value is always the issuing principal or the append time, it is provenance. No linter catches this; it has to be checked in review. +Moving provenance to the envelope is only correct when the platform guarantees it, so confirm each of these before removing a payload field, and if any is missing, fix the infrastructure first instead of dropping the only record: + +- The runtime, not the caller or the domain logic, writes the authenticated principal onto every event a command produces, under a documented, reserved envelope key. +- The runtime or store records the append time on every event under a documented key. +- Both survive everything that moves events: replay, re-appending into another stream, restore from backup or archive, mirroring and replication. A store's own message timestamp is usually the time of that particular write, so re-appending an event elsewhere resets it unless the original is carried forward as metadata. +- Consumers that need these values (an access-control system granting the creator membership, an audit view) have a supported way to read them. + +Until those hold, a payload field is the honest place for the value; document it as a stopgap tied to the missing guarantee. If a pure decider needs the value to emit it, the trusted ingress that authenticated the caller stamps it onto the command, overwriting anything the client sent, the same way a host stamps the time; the client's own claim is never trusted. + +### Recorded Time Is Not Domain Time + +The time an event was recorded and the time the fact happened in the domain are different values, and only the first is provenance: + +- **Recorded time**: when the store appended the event. Envelope, assigned by the infrastructure, never by the caller. +- **Domain time**: when the thing happened, took effect, or was observed: a reading taken by a device that synced hours later, a backfill or import of historical facts, a webhook carrying the external system's own timestamp, the time a scheduled occurrence was due, an effective date for a policy change. This is a domain fact, so it belongs in the payload. + +Name a domain time for what it means (`observed_at`, `effective_at`, `due_at`), not with a generic `occurred_at` or `created_at` that a reader will mistake for recorded time. If recorded time is always the right answer for a use case, there is no domain time to model. + +A decider's `decide` is pure and has no clock. When a decision needs domain time, it arrives in the command from a source the system trusts (a timer, a host-stamped value, a validated external timestamp), never from a client's unchecked `now`. + +An actor field belongs in the payload by the same test: only when it names someone other than the issuer, such as an approver named in a command a system process issues on their behalf. If the value is always the issuing principal or the append time, it is provenance. No linter catches this; it has to be checked in review. ## Record Creation-Time Facts on the Creation Event, Even When Another System Owns What Happens Later @@ -147,7 +167,9 @@ ProjectId project_id = 1; ## Review Questions - Are commands named imperatively and events named in the past tense? -- Does any command or event carry who issued it or when it happened (`*_by`, `creator`, `principal_id`, `created_at`, `occurred_at`) instead of leaving that to the envelope, and does any command let the caller supply its own identity? +- Does any command or event copy who issued it or when it was recorded into the payload, while the infrastructure already guarantees both in the envelope? If the infrastructure does not guarantee them yet, is that the gap being fixed? +- Does any command let the caller supply its own identity? +- Is every payload time a domain time named for its meaning, distinct from recorded time? - Does the creation event carry every fact that is only knowable at creation time, even if later changes to those facts are owned elsewhere? - Does any event represent a fact the aggregate does not own, duplicating another system's source of truth? - Are faults modeled as a `oneof` of specific types rather than a generic error string or code? diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/example-project-schema.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/example-project-schema.md index 659cd63..733e0b5 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/example-project-schema.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/example-project-schema.md @@ -147,7 +147,7 @@ message RenameProjectFault { | `AccessPreset` zero value rejected via `buf.validate.field.enum` | An omitted choice cannot silently widen access | | `CreateProject.name` marked `required` via `string.min_len = 1` | Schema-checkable invariant expressed portably | | `ProjectCreated` carries `parent` and `initial_access_preset` | Creation-time facts recorded even though later changes are owned elsewhere | -| No `created_by`, `created_at`, or `archived_at` on commands or events | Who and when are provenance, recorded in the event envelope | +| No `created_by`, `created_at`, or `archived_at` on commands or events | Who issued it and when it was recorded are provenance the event envelope carries; the project has no separate domain time | | `Project` state omits `parent` and access | Folded state holds only facts the aggregate's own events keep current | | No `ProjectMoved` or `ProjectAccessChanged` event | The aggregate does not emit events for facts it does not own | | `RenameProjectFault` as a `oneof` | Specific, matchable fault types instead of a generic error | diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md index 1d8a0fa..c3f2e94 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md @@ -46,7 +46,9 @@ Each remaining item is a symptom of a principle `buf lint` cannot check, not a r Skip this section for plain CRUD/RPC schemas. - [ ] Commands are imperative; events are past tense -- [ ] No command or event carries provenance (who issued it, when it happened); the envelope does, and no command lets the caller supply its own identity +- [ ] No command lets the caller supply its own identity +- [ ] Provenance (issuing principal, recorded time) stays out of payloads where the infrastructure guarantees it in the envelope, across replay, re-append, and restore +- [ ] Payload times are domain times named for their meaning, never a copy of recorded time - [ ] The creation event carries every fact only knowable at creation time - [ ] No event duplicates a fact owned by another system - [ ] Faults are a `oneof` of specific types, not a generic error string diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md index 03179b5..55d2739 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md @@ -47,7 +47,7 @@ int64 created = 1; google.protobuf.Timestamp created_at = 1; ``` -On an event-sourced command or event, the time it happened is provenance and belongs in the envelope, not a payload `_at` field; see [event-sourced-contracts.md](event-sourced-contracts.md). +On an event-sourced command or event, the time it was recorded is provenance for the envelope, while the time it happened in the domain is a payload fact named for its meaning; see [event-sourced-contracts.md](event-sourced-contracts.md). ## Review Questions From 64937961d7497bb46ada16753447b5ef7ccb5114 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 11 Oct 2026 00:31:59 -0400 Subject: [PATCH 3/4] docs(protobuf): treat provenance as platform metadata on every event so contracts never model it piecemeal Signed-off-by: Yordis Prieto --- .../skills/protobuf-design-messages/SKILL.md | 2 +- .../references/event-sourced-contracts.md | 20 ++++++++++--------- .../references/review-checklist.md | 4 ++-- .../references/units-money-and-time.md | 2 +- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md index 680a854..cea39dc 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md @@ -22,7 +22,7 @@ Run `buf format -w` and `buf lint` (with `STANDARD`, buf's default) before anyth 2. **No primitive obsession.** Identifiers, references, and out-of-line content pointers are value objects, not bare strings; units, money, and instants follow the ecosystem's naming patterns. 3. **Enums encode an exhaustive choice, not a growable boolean.** The zero value is always `_UNSPECIFIED`, code must reject it, and a boolean that might need a third state should be an enum instead. 4. **Know what the schema can and cannot guarantee.** Editions presence and `protovalidate` constraints are schema-level tools with real limits; some invariants need live state and belong in the service. -5. **Event-sourced contracts separate commands, events, state, and faults.** Commands are imperative, events are past tense, provenance (who issued it, when it was recorded) lives in an envelope the infrastructure guarantees, domain time stays in the payload, and an aggregate never gets an event for a fact it does not own. +5. **Event-sourced contracts separate commands, events, state, and faults.** Commands are imperative, events are past tense, provenance (who caused it, when each step happened) is platform metadata on every event's envelope, guaranteed by the infrastructure, domain time stays in the payload, and an aggregate never gets an event for a fact it does not own. 6. **Rename early when words collide with product vocabulary.** The cheapest moment to fix a name is before anything generates code from it. 7. **A file is named after what it defines.** One top-level definition per file in `lower_snake_case`, a service in `_service.proto` named `Service`, and its own `Request` and `Response` for every RPC. diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md index e891924..19ed49d 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md @@ -30,7 +30,7 @@ message ProjectArchived { ... } ## Who and When Belong to the Envelope, Not the Payload -Who issued a command and when its events were recorded are provenance. When the infrastructure records them, keep them in the event's envelope (metadata or headers) and out of the payload. A `created_by`, `requested_by`, `creator`, `principal_id`, or `recorded_at` on a command or event then duplicates the envelope at best, and at worst lets the caller decide whose name goes on the record. +Who caused an event and when each step of its life happened are provenance. They are the same question for every event in the system, so they belong to platform metadata the infrastructure attaches to every event's envelope, not to individual contracts. When the infrastructure records them, keep them out of the payload. A `created_by`, `requested_by`, `creator`, `principal_id`, or `recorded_at` on a command or event then duplicates the envelope at best, and at worst lets the caller decide whose name goes on the record. Commands are where this goes wrong first. The issuing principal comes from authentication, never from a field the caller fills in; a `creator` on `CreateProject` lets anyone claim to be anyone. Copying it onto the event then turns that claim into a recorded fact. @@ -61,25 +61,27 @@ message ProjectArchived { Moving provenance to the envelope is only correct when the platform guarantees it, so confirm each of these before removing a payload field, and if any is missing, fix the infrastructure first instead of dropping the only record: -- The runtime, not the caller or the domain logic, writes the authenticated principal onto every event a command produces, under a documented, reserved envelope key. -- The runtime or store records the append time on every event under a documented key. +- The platform, not the caller or the domain logic, records the authenticated actor on every event, across every contract, with no opt-in. System processes (schedulers, processors) act under identities of their own, so no event lacks an actor. +- The platform records a time for each step that matters, not only the write to storage: when the request was accepted, when the event was produced, and when it was first recorded. Each is stamped by the part of the platform that observed that step. - Both survive everything that moves events: replay, re-appending into another stream, restore from backup or archive, mirroring and replication. A store's own message timestamp is usually the time of that particular write, so re-appending an event elsewhere resets it unless the original is carried forward as metadata. - Consumers that need these values (an access-control system granting the creator membership, an audit view) have a supported way to read them. +Where these values live and how they are encoded is specific to each platform and each store, so this skill names no keys. The platform has to document, in one place, which envelope values it guarantees, where each lives, and what is still missing. Read that before dropping a payload field, and if it does not exist, that is the first gap. + Until those hold, a payload field is the honest place for the value; document it as a stopgap tied to the missing guarantee. If a pure decider needs the value to emit it, the trusted ingress that authenticated the caller stamps it onto the command, overwriting anything the client sent, the same way a host stamps the time; the client's own claim is never trusted. ### Recorded Time Is Not Domain Time -The time an event was recorded and the time the fact happened in the domain are different values, and only the first is provenance: +The times the platform observed an event and the time the fact happened in the domain are different values, and only the first kind is provenance: -- **Recorded time**: when the store appended the event. Envelope, assigned by the infrastructure, never by the caller. +- **Envelope times**: when the request was accepted, when the event was produced, and when it was first recorded. Envelope, assigned by the infrastructure, never by the caller. - **Domain time**: when the thing happened, took effect, or was observed: a reading taken by a device that synced hours later, a backfill or import of historical facts, a webhook carrying the external system's own timestamp, the time a scheduled occurrence was due, an effective date for a policy change. This is a domain fact, so it belongs in the payload. -Name a domain time for what it means (`observed_at`, `effective_at`, `due_at`), not with a generic `occurred_at` or `created_at` that a reader will mistake for recorded time. If recorded time is always the right answer for a use case, there is no domain time to model. +Name a domain time for what it means (`observed_at`, `effective_at`, `due_at`), not with a generic `occurred_at` or `created_at` that a reader will mistake for an envelope time. If an envelope time is always the right answer for a use case, there is no domain time to model. A decider's `decide` is pure and has no clock. When a decision needs domain time, it arrives in the command from a source the system trusts (a timer, a host-stamped value, a validated external timestamp), never from a client's unchecked `now`. -An actor field belongs in the payload by the same test: only when it names someone other than the issuer, such as an approver named in a command a system process issues on their behalf. If the value is always the issuing principal or the append time, it is provenance. No linter catches this; it has to be checked in review. +An actor field belongs in the payload by the same test: only when it names someone other than the issuer, such as an approver named in a command a system process issues on their behalf. If the value is always the actor that caused the event or one of the envelope times, it is provenance. No linter catches this; it has to be checked in review. ## Record Creation-Time Facts on the Creation Event, Even When Another System Owns What Happens Later @@ -167,9 +169,9 @@ ProjectId project_id = 1; ## Review Questions - Are commands named imperatively and events named in the past tense? -- Does any command or event copy who issued it or when it was recorded into the payload, while the infrastructure already guarantees both in the envelope? If the infrastructure does not guarantee them yet, is that the gap being fixed? +- Does any command or event copy who caused it or when it was accepted, produced, or recorded into the payload, while the platform already guarantees those on every event's envelope? If the infrastructure does not guarantee them yet, is that the gap being fixed? - Does any command let the caller supply its own identity? -- Is every payload time a domain time named for its meaning, distinct from recorded time? +- Is every payload time a domain time named for its meaning, distinct from the envelope times? - Does the creation event carry every fact that is only knowable at creation time, even if later changes to those facts are owned elsewhere? - Does any event represent a fact the aggregate does not own, duplicating another system's source of truth? - Are faults modeled as a `oneof` of specific types rather than a generic error string or code? diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md index c3f2e94..b805b1e 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md @@ -47,8 +47,8 @@ Skip this section for plain CRUD/RPC schemas. - [ ] Commands are imperative; events are past tense - [ ] No command lets the caller supply its own identity -- [ ] Provenance (issuing principal, recorded time) stays out of payloads where the infrastructure guarantees it in the envelope, across replay, re-append, and restore -- [ ] Payload times are domain times named for their meaning, never a copy of recorded time +- [ ] Provenance (the actor that caused the event, the envelope times) stays out of payloads where the platform guarantees it on every event's envelope, across replay, re-append, and restore +- [ ] Payload times are domain times named for their meaning, never a copy of an envelope time - [ ] The creation event carries every fact only knowable at creation time - [ ] No event duplicates a fact owned by another system - [ ] Faults are a `oneof` of specific types, not a generic error string diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md index 55d2739..8d9e93a 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md @@ -47,7 +47,7 @@ int64 created = 1; google.protobuf.Timestamp created_at = 1; ``` -On an event-sourced command or event, the time it was recorded is provenance for the envelope, while the time it happened in the domain is a payload fact named for its meaning; see [event-sourced-contracts.md](event-sourced-contracts.md). +On an event-sourced command or event, the times the platform observed it (accepted, produced, recorded) are provenance for the envelope, while the time it happened in the domain is a payload fact named for its meaning; see [event-sourced-contracts.md](event-sourced-contracts.md). ## Review Questions From 4e7d951ea5b1721c1620e4314ae634dabaf0bd95 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 11 Oct 2026 00:36:35 -0400 Subject: [PATCH 4/4] docs(protobuf): keep producer-reported occurrence time in event context so the skills agree on where it lives Signed-off-by: Yordis Prieto --- .../skills/protobuf-design-messages/SKILL.md | 2 +- .../references/event-sourced-contracts.md | 9 +++++---- .../references/review-checklist.md | 2 +- .../references/units-money-and-time.md | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md index cea39dc..c7f94de 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/SKILL.md @@ -22,7 +22,7 @@ Run `buf format -w` and `buf lint` (with `STANDARD`, buf's default) before anyth 2. **No primitive obsession.** Identifiers, references, and out-of-line content pointers are value objects, not bare strings; units, money, and instants follow the ecosystem's naming patterns. 3. **Enums encode an exhaustive choice, not a growable boolean.** The zero value is always `_UNSPECIFIED`, code must reject it, and a boolean that might need a third state should be an enum instead. 4. **Know what the schema can and cannot guarantee.** Editions presence and `protovalidate` constraints are schema-level tools with real limits; some invariants need live state and belong in the service. -5. **Event-sourced contracts separate commands, events, state, and faults.** Commands are imperative, events are past tense, provenance (who caused it, when each step happened) is platform metadata on every event's envelope, guaranteed by the infrastructure, domain time stays in the payload, and an aggregate never gets an event for a fact it does not own. +5. **Event-sourced contracts separate commands, events, state, and faults.** Commands are imperative, events are past tense, provenance (who caused it, when each step happened) is platform metadata on every event's envelope, guaranteed by the infrastructure, occurrence time is event context, only domain time stays in the payload, and an aggregate never gets an event for a fact it does not own. 6. **Rename early when words collide with product vocabulary.** The cheapest moment to fix a name is before anything generates code from it. 7. **A file is named after what it defines.** One top-level definition per file in `lower_snake_case`, a service in `_service.proto` named `Service`, and its own `Request` and `Response` for every RPC. diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md index 19ed49d..e4f57f2 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/event-sourced-contracts.md @@ -72,12 +72,13 @@ Until those hold, a payload field is the honest place for the value; document it ### Recorded Time Is Not Domain Time -The times the platform observed an event and the time the fact happened in the domain are different values, and only the first kind is provenance: +Times about the event itself and times inside the business fact are different values, and only the first kind is provenance: -- **Envelope times**: when the request was accepted, when the event was produced, and when it was first recorded. Envelope, assigned by the infrastructure, never by the caller. -- **Domain time**: when the thing happened, took effect, or was observed: a reading taken by a device that synced hours later, a backfill or import of historical facts, a webhook carrying the external system's own timestamp, the time a scheduled occurrence was due, an effective date for a policy change. This is a domain fact, so it belongs in the payload. +- **Envelope times**: when the request was accepted, when the event was produced, and when it was first recorded. Assigned by the infrastructure, never by the caller. +- **Occurrence time**: when the producer says the event happened (`occurred_at`), which differs from recorded time for a device that synced hours later, a backfill or import of historical facts, or a webhook carrying the external system's own timestamp. Every event has one, so it is event context in the envelope, kept distinct from recorded time, and recorded as the producer's claim rather than a platform observation. +- **Domain time**: a time that is part of the business fact itself, such as when a scheduled occurrence was due, when a policy change takes effect, or when a measurement was taken. This belongs in the payload. -Name a domain time for what it means (`observed_at`, `effective_at`, `due_at`), not with a generic `occurred_at` or `created_at` that a reader will mistake for an envelope time. If an envelope time is always the right answer for a use case, there is no domain time to model. +Name a domain time for what it means (`due_at`, `effective_at`, `measured_at`), never with a generic `occurred_at` or `created_at` that a reader will mistake for an envelope or occurrence time. If an envelope or occurrence time is always the right answer for a use case, there is no domain time to model. See the EDA skill's event record layers for the same split between record metadata, event context, and payload. A decider's `decide` is pure and has no clock. When a decision needs domain time, it arrives in the command from a source the system trusts (a timer, a host-stamped value, a validated external timestamp), never from a client's unchecked `now`. diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md index b805b1e..21a9884 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/review-checklist.md @@ -48,7 +48,7 @@ Skip this section for plain CRUD/RPC schemas. - [ ] Commands are imperative; events are past tense - [ ] No command lets the caller supply its own identity - [ ] Provenance (the actor that caused the event, the envelope times) stays out of payloads where the platform guarantees it on every event's envelope, across replay, re-append, and restore -- [ ] Payload times are domain times named for their meaning, never a copy of an envelope time +- [ ] Payload times are domain times named for their meaning, never a copy of an envelope time or the producer-reported occurrence time - [ ] The creation event carries every fact only knowable at creation time - [ ] No event duplicates a fact owned by another system - [ ] Faults are a `oneof` of specific types, not a generic error string diff --git a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md index 8d9e93a..c682890 100644 --- a/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md +++ b/plugins/trogonstack-protobuf/skills/protobuf-design-messages/references/units-money-and-time.md @@ -47,7 +47,7 @@ int64 created = 1; google.protobuf.Timestamp created_at = 1; ``` -On an event-sourced command or event, the times the platform observed it (accepted, produced, recorded) are provenance for the envelope, while the time it happened in the domain is a payload fact named for its meaning; see [event-sourced-contracts.md](event-sourced-contracts.md). +On an event-sourced command or event, the times the platform observed it (accepted, produced, recorded) are provenance for the envelope, the producer-reported occurrence time is event context in the envelope, and only a time that is part of the business fact (due, effective, measured) is a payload field named for its meaning; see [event-sourced-contracts.md](event-sourced-contracts.md). ## Review Questions