From 8ab530afb819056b94695af4c7b58eb7d876a9cb Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Mon, 19 Oct 2026 11:18:30 -0400 Subject: [PATCH 1/5] fix(examples): make acme-storefront pass event model validation Signed-off-by: Yordis Prieto --- .../manifests/acme-storefront/command.yaml | 19 ++++++ .../acme-storefront/event-model.yaml | 20 ++++-- examples/manifests/acme-storefront/event.yaml | 12 ++++ .../manifests/acme-storefront/persona.yaml | 3 + .../acme-storefront/read-model-slice.yaml | 40 +++++++++++- .../manifests/acme-storefront/read-model.yaml | 61 ++++++++++++++++--- .../manifests/acme-storefront/storyboard.yaml | 24 ++++++-- 7 files changed, 158 insertions(+), 21 deletions(-) diff --git a/examples/manifests/acme-storefront/command.yaml b/examples/manifests/acme-storefront/command.yaml index dfd833e7..7b2d4404 100644 --- a/examples/manifests/acme-storefront/command.yaml +++ b/examples/manifests/acme-storefront/command.yaml @@ -40,6 +40,19 @@ spec: - name: cart_id type: uuid: {} + - name: buyer_id + type: + uuid: {} + - name: seller_id + type: + uuid: {} + - metadata: + - '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.DerivedFieldAnnotation + doc: now() when the order is recorded + kind: KIND_TIMESTAMP + name: placed_at + type: + timestamp: {} swimlane: id: namespace: acme-storefront @@ -60,6 +73,12 @@ spec: - name: order_id type: uuid: {} + - name: buyer_id + type: + uuid: {} + - name: seller_id + type: + uuid: {} - metadata: - '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.DerivedFieldAnnotation doc: now() when the worker records the outcome diff --git a/examples/manifests/acme-storefront/event-model.yaml b/examples/manifests/acme-storefront/event-model.yaml index 5db45b4f..348119d4 100644 --- a/examples/manifests/acme-storefront/event-model.yaml +++ b/examples/manifests/acme-storefront/event-model.yaml @@ -186,11 +186,6 @@ spec: slug: fulfillment-queue-rms version: '1' kind: ENTITY_KIND_READ_MODEL_SLICE - - id: - namespace: acme-storefront - slug: order-status-rms - version: '1' - kind: ENTITY_KIND_READ_MODEL_SLICE - id: namespace: acme-storefront slug: capture-payment-auto @@ -216,4 +211,19 @@ spec: slug: fulfillment-flow version: '1' kind: ENTITY_KIND_STORYBOARD + - id: + namespace: acme-storefront + slug: order-status-placed-rms + version: '1' + kind: ENTITY_KIND_READ_MODEL_SLICE + - id: + namespace: acme-storefront + slug: order-status-paid-rms + version: '1' + kind: ENTITY_KIND_READ_MODEL_SLICE + - id: + namespace: acme-storefront + slug: order-status-fulfilled-rms + version: '1' + kind: ENTITY_KIND_READ_MODEL_SLICE title: Acme Storefront diff --git a/examples/manifests/acme-storefront/event.yaml b/examples/manifests/acme-storefront/event.yaml index f9d56b9d..06959cfe 100644 --- a/examples/manifests/acme-storefront/event.yaml +++ b/examples/manifests/acme-storefront/event.yaml @@ -40,6 +40,12 @@ spec: - name: cart_id type: uuid: {} + - name: buyer_id + type: + uuid: {} + - name: seller_id + type: + uuid: {} - name: placed_at type: timestamp: {} @@ -63,6 +69,12 @@ spec: - name: order_id type: uuid: {} + - name: buyer_id + type: + uuid: {} + - name: seller_id + type: + uuid: {} - name: paid_at type: timestamp: {} diff --git a/examples/manifests/acme-storefront/persona.yaml b/examples/manifests/acme-storefront/persona.yaml index a2a9a1d3..9d7d09b6 100644 --- a/examples/manifests/acme-storefront/persona.yaml +++ b/examples/manifests/acme-storefront/persona.yaml @@ -15,5 +15,8 @@ metadata: namespace: acme-storefront spec: doc: Internal operator who answers order-status questions. + metadata: + - '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.OrphanAnnotation + doc: Reserved for the support console. Its order lookup flow is not modeled yet. role: operator title: Support Agent diff --git a/examples/manifests/acme-storefront/read-model-slice.yaml b/examples/manifests/acme-storefront/read-model-slice.yaml index 96c347c8..bbe186f2 100644 --- a/examples/manifests/acme-storefront/read-model-slice.yaml +++ b/examples/manifests/acme-storefront/read-model-slice.yaml @@ -67,10 +67,10 @@ spec: apiVersion: eventmodel.atlas.trogonstack.com/v1alpha1 kind: ReadModelSlice metadata: - name: order-status-rms + name: order-status-placed-rms namespace: acme-storefront spec: - doc: Each lifecycle event updates the shopper-facing status. + doc: Status row on placement. projectionRole: PROJECTION_ROLE_UPSERT readModel: readModel: @@ -84,14 +84,48 @@ spec: namespace: acme-storefront slug: order.placed version: '1' + title: 'Projection: order placed' +--- +apiVersion: eventmodel.atlas.trogonstack.com/v1alpha1 +kind: ReadModelSlice +metadata: + name: order-status-paid-rms + namespace: acme-storefront +spec: + doc: Status row on payment. + projectionRole: PROJECTION_ROLE_UPSERT + readModel: + readModel: + id: + namespace: acme-storefront + slug: order-status + version: '1' + sourceEvents: - event: id: namespace: acme-storefront slug: order.paid version: '1' + title: 'Projection: order paid' +--- +apiVersion: eventmodel.atlas.trogonstack.com/v1alpha1 +kind: ReadModelSlice +metadata: + name: order-status-fulfilled-rms + namespace: acme-storefront +spec: + doc: Status row on delivery. + projectionRole: PROJECTION_ROLE_UPSERT + readModel: + readModel: + id: + namespace: acme-storefront + slug: order-status + version: '1' + sourceEvents: - event: id: namespace: acme-storefront slug: order.fulfilled version: '1' - title: 'Projection: order status' + title: 'Projection: order fulfilled' diff --git a/examples/manifests/acme-storefront/read-model.yaml b/examples/manifests/acme-storefront/read-model.yaml index 6ec557e2..6fb21f30 100644 --- a/examples/manifests/acme-storefront/read-model.yaml +++ b/examples/manifests/acme-storefront/read-model.yaml @@ -5,6 +5,8 @@ metadata: namespace: acme-storefront spec: doc: Running item count per cart. + key: + - cart_id schema: '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.Schema fields: @@ -14,8 +16,12 @@ spec: - name: item_count type: int: {} - key: - - cart_id + - name: product_id + type: + uuid: {} + - name: quantity + type: + int: {} sourceEvents: - id: namespace: acme-storefront @@ -30,14 +36,29 @@ metadata: namespace: acme-storefront spec: doc: 'Worker feed: orders awaiting payment capture.' + key: + - order_id schema: '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.Schema fields: - name: order_id type: uuid: {} - key: - - order_id + - name: buyer_id + type: + uuid: {} + - name: seller_id + type: + uuid: {} + - name: cart_id + type: + uuid: {} + - name: placed_at + type: + timestamp: {} + - name: status + type: + string: {} sourceEvents: - id: namespace: acme-storefront @@ -52,6 +73,8 @@ metadata: namespace: acme-storefront spec: doc: 'Worker feed: paid orders awaiting carrier dispatch.' + key: + - order_id schema: '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.Schema fields: @@ -64,8 +87,12 @@ spec: - name: seller_id type: uuid: {} - key: - - order_id + - name: paid_at + type: + timestamp: {} + - name: status + type: + string: {} sourceEvents: - id: namespace: acme-storefront @@ -80,6 +107,8 @@ metadata: namespace: acme-storefront spec: doc: Current status of an order for the shopper-facing tracking page. + key: + - order_id schema: '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.Schema fields: @@ -89,8 +118,24 @@ spec: - name: status type: string: {} - key: - - order_id + - name: cart_id + type: + uuid: {} + - name: buyer_id + type: + uuid: {} + - name: seller_id + type: + uuid: {} + - name: placed_at + type: + timestamp: {} + - name: paid_at + type: + timestamp: {} + - name: fulfilled_at + type: + timestamp: {} sourceEvents: - id: namespace: acme-storefront diff --git a/examples/manifests/acme-storefront/storyboard.yaml b/examples/manifests/acme-storefront/storyboard.yaml index 01dcb7b3..10182ed8 100644 --- a/examples/manifests/acme-storefront/storyboard.yaml +++ b/examples/manifests/acme-storefront/storyboard.yaml @@ -19,6 +19,12 @@ spec: namespace: acme-storefront slug: storefront-checkout version: '1' + readModel: + readModel: + id: + namespace: acme-storefront + slug: cart-summary + version: '1' outcome: events: - event: @@ -39,6 +45,14 @@ spec: namespace: acme-storefront slug: place-order-slice version: '1' + - id: + namespace: acme-storefront + slug: pending-payments-rms + version: '1' + - id: + namespace: acme-storefront + slug: order-status-placed-rms + version: '1' title: Checkout --- apiVersion: eventmodel.atlas.trogonstack.com/v1alpha1 @@ -47,7 +61,7 @@ metadata: name: fulfillment-flow namespace: acme-storefront spec: - doc: A placed order is captured, dispatched, and confirmed delivered. + doc: A placed order is captured, dispatched, and confirmed delivered. It starts from the pending-payments queue that checkout-flow populated. entry: automation: processor: @@ -72,15 +86,15 @@ spec: slices: - id: namespace: acme-storefront - slug: pending-payments-rms + slug: capture-payment-auto version: '1' - id: namespace: acme-storefront - slug: capture-payment-auto + slug: capture-payment-slice version: '1' - id: namespace: acme-storefront - slug: capture-payment-slice + slug: order-status-paid-rms version: '1' - id: namespace: acme-storefront @@ -96,7 +110,7 @@ spec: version: '1' - id: namespace: acme-storefront - slug: order-status-rms + slug: order-status-fulfilled-rms version: '1' - id: namespace: acme-storefront From c592839e0c4de90bfe8365833f09232762020117 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Mon, 19 Oct 2026 11:18:31 -0400 Subject: [PATCH 2/5] fix(examples): make acme-reviews pass event model validation Signed-off-by: Yordis Prieto --- .../manifests/acme-reviews/command-slice.yaml | 6 ---- examples/manifests/acme-reviews/command.yaml | 7 +++++ .../manifests/acme-reviews/event-model.yaml | 5 ---- .../manifests/acme-reviews/read-model.yaml | 29 ++++++++++++++++--- .../manifests/acme-reviews/storyboard.yaml | 12 ++++---- examples/manifests/acme-reviews/ui.yaml | 3 ++ 6 files changed, 42 insertions(+), 20 deletions(-) diff --git a/examples/manifests/acme-reviews/command-slice.yaml b/examples/manifests/acme-reviews/command-slice.yaml index 13e99051..58867b94 100644 --- a/examples/manifests/acme-reviews/command-slice.yaml +++ b/examples/manifests/acme-reviews/command-slice.yaml @@ -26,12 +26,6 @@ spec: namespace: acme-reviews slug: review-eligibility.granted version: '1' - given: - - event: - id: - namespace: acme-storefront - slug: order.fulfilled - version: '1' id: delivered title: Eligibility granted when: diff --git a/examples/manifests/acme-reviews/command.yaml b/examples/manifests/acme-reviews/command.yaml index 7e8ca02b..b5c4fb4f 100644 --- a/examples/manifests/acme-reviews/command.yaml +++ b/examples/manifests/acme-reviews/command.yaml @@ -50,6 +50,13 @@ spec: - name: comment type: string: {} + - metadata: + - '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.DerivedFieldAnnotation + doc: now() when the review is recorded + kind: KIND_TIMESTAMP + name: submitted_at + type: + timestamp: {} swimlane: id: namespace: acme-reviews diff --git a/examples/manifests/acme-reviews/event-model.yaml b/examples/manifests/acme-reviews/event-model.yaml index be702931..d6074339 100644 --- a/examples/manifests/acme-reviews/event-model.yaml +++ b/examples/manifests/acme-reviews/event-model.yaml @@ -81,11 +81,6 @@ spec: slug: submit-review-slice version: '1' kind: ENTITY_KIND_COMMAND_SLICE - - id: - namespace: acme-reviews - slug: orders-delivered-unreviewed-upsert-rms - version: '1' - kind: ENTITY_KIND_READ_MODEL_SLICE - id: namespace: acme-reviews slug: orders-delivered-unreviewed-delete-rms diff --git a/examples/manifests/acme-reviews/read-model.yaml b/examples/manifests/acme-reviews/read-model.yaml index 4747a3c2..79ae6229 100644 --- a/examples/manifests/acme-reviews/read-model.yaml +++ b/examples/manifests/acme-reviews/read-model.yaml @@ -5,6 +5,8 @@ metadata: namespace: acme-reviews spec: doc: 'Worker feed: delivered orders without a review-eligibility grant yet.' + key: + - order_id schema: '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.Schema fields: @@ -17,8 +19,15 @@ spec: - name: seller_id type: uuid: {} - key: - - order_id + - name: fulfilled_at + type: + timestamp: {} + - name: granted_at + type: + timestamp: {} + - name: status + type: + string: {} sourceEvents: - id: namespace: acme-storefront @@ -37,6 +46,8 @@ metadata: namespace: acme-reviews spec: doc: Orders the reviewer-facing form can still collect a review for. + key: + - order_id schema: '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.Schema fields: @@ -52,8 +63,18 @@ spec: - name: granted_at type: timestamp: {} - key: - - order_id + - name: status + type: + string: {} + - name: rating + type: + int: {} + - name: comment + type: + string: {} + - name: submitted_at + type: + timestamp: {} sourceEvents: - id: namespace: acme-reviews diff --git a/examples/manifests/acme-reviews/storyboard.yaml b/examples/manifests/acme-reviews/storyboard.yaml index 59e52df9..41ca8608 100644 --- a/examples/manifests/acme-reviews/storyboard.yaml +++ b/examples/manifests/acme-reviews/storyboard.yaml @@ -4,7 +4,7 @@ metadata: name: review-eligibility-grant namespace: acme-reviews spec: - doc: A delivered order clears the no-review window and becomes reviewable. + doc: A delivered order clears the no-review window and becomes reviewable. It starts from the delivered-unreviewed queue that the storefront fulfillment event populates, so that projection is upstream preparation and not a step here. entry: automation: processor: @@ -27,10 +27,6 @@ spec: slug: review-eligibility.granted version: '1' slices: - - id: - namespace: acme-reviews - slug: orders-delivered-unreviewed-upsert-rms - version: '1' - id: namespace: acme-reviews slug: grant-eligibility-auto @@ -70,6 +66,12 @@ spec: namespace: acme-reviews slug: review-form version: '1' + readModel: + readModel: + id: + namespace: acme-reviews + slug: eligible-reviews + version: '1' outcome: events: - event: diff --git a/examples/manifests/acme-reviews/ui.yaml b/examples/manifests/acme-reviews/ui.yaml index 3718ce6a..09dec3c3 100644 --- a/examples/manifests/acme-reviews/ui.yaml +++ b/examples/manifests/acme-reviews/ui.yaml @@ -5,4 +5,7 @@ metadata: namespace: acme-reviews spec: doc: 'Reviewer-facing: rate and comment on a delivered order.' + metadata: + - '@type': type.googleapis.com/trogonatlas.eventmodel.v1alpha1.MixedSurfaceAnnotation + doc: The form is prefilled from the eligible-reviews row it submits against, so one surface both displays the order and takes the review. title: Review form From 5d2a4d9b836be8a8c059f59cde4277f9658fb6a8 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Mon, 19 Oct 2026 11:18:31 -0400 Subject: [PATCH 3/5] fix(examples): give the deletion storyboard an entry read model Signed-off-by: Yordis Prieto --- .../manifests/acme-customer-data-deletion/storyboard.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/manifests/acme-customer-data-deletion/storyboard.yaml b/examples/manifests/acme-customer-data-deletion/storyboard.yaml index 61ad8746..7f667b2e 100644 --- a/examples/manifests/acme-customer-data-deletion/storyboard.yaml +++ b/examples/manifests/acme-customer-data-deletion/storyboard.yaml @@ -19,6 +19,12 @@ spec: namespace: acme-customer-data-deletion slug: deletion-request-form version: '1' + readModel: + readModel: + id: + namespace: acme-customer-data-deletion + slug: pending-deletions + version: '1' outcome: events: - event: From 13d0cf360292ca0b98e19c2f75cbbdd2455b0d70 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Mon, 19 Oct 2026 11:18:31 -0400 Subject: [PATCH 4/5] chore(test): fail when an example manifest stops validating Signed-off-by: Yordis Prieto --- .../explanation/validation-repair-patterns.md | 71 ++++++++++++ docs/how-to/repair-validation-findings.md | 7 ++ .../tests/example_manifests_validate.rs | 103 ++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 rsworkspace/crates/trogon-atlas-server/tests/example_manifests_validate.rs diff --git a/docs/explanation/validation-repair-patterns.md b/docs/explanation/validation-repair-patterns.md index 1c39ef1a..622ae230 100644 --- a/docs/explanation/validation-repair-patterns.md +++ b/docs/explanation/validation-repair-patterns.md @@ -157,3 +157,74 @@ would change behavior, verification, and limits. Link to existing schema or fixture definitions when useful, and distinguish historical results from current validation. Amend an existing pattern when the new case refines its applicability rather than copying the same procedure. + +## Example manifests held to the validator + +### Findings on unmodified main + +The manifests under `examples/manifests/` were only checked against the +manifest schema, so they drifted from the validator and taught patterns it +rejects. `example_manifests_validate` in `trogon-atlas-server` now applies all +three namespaces to an ephemeral store and requires every model to validate +with no warning or error. + +On unmodified main the three models reported these findings. + +| Model | Errors | Warnings | +| --- | --- | --- | +| `acme-storefront` | 12 | 2 | +| `acme-reviews` | 7 | 2 | +| `acme-customer-data-deletion` | 0 | 1 | + +### Decisions + +- **System-recorded timestamp.** `EVENT_FIELD_UNSOURCED` on + `order.placed.placed_at` and `review.submitted.submitted_at` is a real gap. + The command now declares the field with a `DerivedFieldAnnotation`, the + way `capture-payment.paid_at` already did. +- **Fields a downstream view needs travel with the events.** + `READ_MODEL_FIELD_UNSOURCED` on `fulfillment-queue.buyer_id` and + `seller_id` was repaired by carrying both from `place-order` through + `order.placed`, `pending-payments`, `capture-payment` and `order.paid`. + Dropping the fields would have removed what the dispatch automation hands + to `mark-fulfilled`. +- **A view must carry every field of each source event.** + `RM_SOURCE_EVENT_FIELD_NOT_PROJECTED` is repaired by adding the missing + fields, including swimlane state such as `status`. +- **One projection per emitter.** A read model fed by several events + (`order-status`) cannot sit in one slice: `PROJECTION_BEFORE_EMITTER` and + `PROJECTION_NOT_ADJACENT` both fire. It is now one read-model slice per + source event, each placed right after the command slice that emits it. +- **A queue filled in an earlier storyboard.** `pending-payments-rms` moved + into `checkout-flow` after `place-order-slice`. `fulfillment-flow` still + enters on that queue. This is the same-context sibling of the + [upstream subscription pattern](#upstream-subscription-at-a-local-storyboard-entry): + there the emitter lives in another bounded context, here in another + storyboard of the same model, so the projection can follow its emitter + instead of leaving the model. +- **Seed catches up with the recorded acme-reviews repair.** The upstream + fulfillment projection left the storyboard and the model membership, and + the storyboard doc says why. +- **Contexts touch through subscriptions only.** + `CROSS_MODEL_BOUNDARY_VIOLATION` on the grant command slice scenario was + repaired by dropping the `given` that named an `acme-storefront` event. The + automation slice scenario already starts from the subscribed queue. +- **Entry read model.** `STORYBOARD_MISSING_ENTRY` is repaired by naming the + read model the entry observer sees: `cart-summary`, `eligible-reviews` and + `pending-deletions`. +- **Declared intent.** `ORPHAN_NOT_FLAGGED` on `support-agent` and + `UI_USED_AS_INPUT_AND_OUTPUT` on `review-form` are kept and annotated + (`OrphanAnnotation`, `MixedSurfaceAnnotation`) because the persona is + reserved for a flow not modeled yet and the form is prefilled from the row + it submits against. + +### Rule observations, not changed here + +- `RM_SOURCE_EVENT_FIELD_NOT_PROJECTED` cannot express a view that keeps a + count and ignores the event payload: `cart-summary` now also holds the last + `product_id` and `quantity`. +- `STORYBOARD_MISSING_ENTRY` asks for an observed read model even when the + flow starts at a UI that has nothing to display yet, as in `checkout-flow`. +- `trogon-atlas fmt --check` already reports + `acme-customer-data-deletion/read-model.yaml` as unformatted on main. That + file is left alone. diff --git a/docs/how-to/repair-validation-findings.md b/docs/how-to/repair-validation-findings.md index 22d84cf5..d9880309 100644 --- a/docs/how-to/repair-validation-findings.md +++ b/docs/how-to/repair-validation-findings.md @@ -181,6 +181,13 @@ manifest replay can overwrite unrelated live changes. Preview the selected seed manifests with `trogon-atlas apply --dry-run` in the same branch and endpoint. That check verifies manifest admissibility, not full scoped validation. +When the seed itself is the thing to repair, edit the YAML, run +`trogon-atlas fmt -w` on the touched files, and run +`cargo test -p trogon-atlas-server --test example_manifests_validate`. That +test applies every example namespace to an ephemeral store and lists each +warning or error by code and subject, so it doubles as the before and after +record. Finish with `mise run manifests:verify -- bootstrap`. + ## Keep the result available to the next session Before cleanup or handoff, save a descriptive run record in a directory diff --git a/rsworkspace/crates/trogon-atlas-server/tests/example_manifests_validate.rs b/rsworkspace/crates/trogon-atlas-server/tests/example_manifests_validate.rs new file mode 100644 index 00000000..ecaa13b4 --- /dev/null +++ b/rsworkspace/crates/trogon-atlas-server/tests/example_manifests_validate.rs @@ -0,0 +1,103 @@ +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::large_futures)] + +use std::{path::PathBuf, sync::Arc}; + +use tonic::Request; +use trogon_atlas_proto as pb; +use trogon_atlas_proto::event_model_service_server::EventModelService; +use trogon_atlas_server::{metrics::ServerMetrics, service::EventModelServiceImpl}; + +const NAMESPACES: [(&str, &str); 3] = [ + ("acme-storefront", "acme-storefront-model"), + ("acme-reviews", "acme-reviews-model"), + ( + "acme-customer-data-deletion", + "acme-customer-data-deletion-pipeline", + ), +]; + +fn manifests_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../examples/manifests") +} + +fn put(entity: pb::Entity) -> pb::BatchMutateOp { + pb::BatchMutateOp { + op: Some(pb::batch_mutate_op::Op::Put(pb::PutEntityRequest { + operation_id: String::new(), + entity: Some(entity), + create_only: true, + if_match: String::new(), + validate_only: false, + force: false, + })), + } +} + +async fn seed_all_examples() -> EventModelServiceImpl { + let paths: Vec = NAMESPACES + .iter() + .map(|(ns, _)| manifests_dir().join(ns)) + .collect(); + let ops: Vec = trogon_atlas_client::manifest::load_paths(&paths) + .expect("example manifests should load") + .into_iter() + .map(|m| put(m.entity)) + .collect(); + let nats = trogon_atlas_testsupport::shared().await; + let store = nats.store().await; + let svc = EventModelServiceImpl::try_new(Arc::new(store), ServerMetrics::default()).unwrap(); + let resp = svc + .batch_mutate(Request::new(pb::BatchMutateRequest { + operation_id: String::new(), + ops, + validate_only: false, + })) + .await + .expect("batch_mutate failed") + .into_inner(); + assert_eq!( + resp.status, + pb::batch_mutate_response::Status::Applied as i32, + "example seeding must succeed; failure: {:?}", + resp.failure + ); + svc +} + +#[tokio::test(flavor = "multi_thread")] +async fn example_models_raise_no_errors_or_warnings() { + let svc = seed_all_examples().await; + let mut findings = Vec::new(); + for (ns, slug) in NAMESPACES { + let issues = svc + .validate_event_model(Request::new(pb::ValidateEventModelRequest { + event_model: None, + event_model_id: Some(pb::Id { + namespace: ns.into(), + slug: slug.into(), + version: 1, + }), + })) + .await + .unwrap() + .into_inner() + .issues; + for i in issues { + if i.severity >= pb::validation_issue::Severity::Warning as i32 { + findings.push(format!( + "{ns} {:?} {} {:?} {} :: {}", + pb::validation_issue::Severity::try_from(i.severity).unwrap(), + i.code, + i.subject, + i.subject_field, + i.message + )); + } + } + } + assert!( + findings.is_empty(), + "examples must validate cleanly:\n{}", + findings.join("\n") + ); +} From ce289781051e88383bc00c7daa4f4f63feca8075 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Mon, 19 Oct 2026 11:26:43 -0400 Subject: [PATCH 5/5] chore(examples): declare each example project and drop an unused projection Signed-off-by: Yordis Prieto --- .../explanation/validation-repair-patterns.md | 6 +++- .../acme-customer-data-deletion/domain.yaml | 5 +++ .../acme-customer-data-deletion/project.yaml | 8 +++++ examples/manifests/acme-reviews/domain.yaml | 5 +++ examples/manifests/acme-reviews/project.yaml | 8 +++++ .../acme-reviews/read-model-slice.yaml | 22 ------------- .../manifests/acme-storefront/domain.yaml | 5 +++ .../manifests/acme-storefront/project.yaml | 8 +++++ .../tests/example_manifests_validate.rs | 32 +++++++++++++++++++ 9 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 examples/manifests/acme-customer-data-deletion/project.yaml create mode 100644 examples/manifests/acme-reviews/project.yaml create mode 100644 examples/manifests/acme-storefront/project.yaml diff --git a/docs/explanation/validation-repair-patterns.md b/docs/explanation/validation-repair-patterns.md index 622ae230..b24dbdf3 100644 --- a/docs/explanation/validation-repair-patterns.md +++ b/docs/explanation/validation-repair-patterns.md @@ -204,7 +204,11 @@ On unmodified main the three models reported these findings. instead of leaving the model. - **Seed catches up with the recorded acme-reviews repair.** The upstream fulfillment projection left the storyboard and the model membership, and - the storyboard doc says why. + the storyboard doc says why. The projection slice itself is deleted from + the seed because no storyboard, model, scenario, or test references it. +- **Each example declares its Project.** `DOMAIN_MISSING_PROJECT` only shows + in project-scoped validation. Each namespace now has a Project of the same + name that its Domain points to, and the test validates every Project. - **Contexts touch through subscriptions only.** `CROSS_MODEL_BOUNDARY_VIOLATION` on the grant command slice scenario was repaired by dropping the `given` that named an `acme-storefront` event. The diff --git a/examples/manifests/acme-customer-data-deletion/domain.yaml b/examples/manifests/acme-customer-data-deletion/domain.yaml index 258e0f07..e1f7e621 100644 --- a/examples/manifests/acme-customer-data-deletion/domain.yaml +++ b/examples/manifests/acme-customer-data-deletion/domain.yaml @@ -5,4 +5,9 @@ metadata: namespace: acme-customer-data-deletion spec: doc: Honoring an account owner's request to delete the personal data Acme holds about them. + project: + id: + namespace: acme-customer-data-deletion + slug: acme-customer-data-deletion + version: '1' title: Customer Privacy diff --git a/examples/manifests/acme-customer-data-deletion/project.yaml b/examples/manifests/acme-customer-data-deletion/project.yaml new file mode 100644 index 00000000..ad5f4b36 --- /dev/null +++ b/examples/manifests/acme-customer-data-deletion/project.yaml @@ -0,0 +1,8 @@ +apiVersion: eventmodel.atlas.trogonstack.com/v1alpha1 +kind: Project +metadata: + name: acme-customer-data-deletion + namespace: acme-customer-data-deletion +spec: + doc: Customer data deletion across every downstream vendor. + title: Acme Customer Data Deletion diff --git a/examples/manifests/acme-reviews/domain.yaml b/examples/manifests/acme-reviews/domain.yaml index df4a298c..474facff 100644 --- a/examples/manifests/acme-reviews/domain.yaml +++ b/examples/manifests/acme-reviews/domain.yaml @@ -5,4 +5,9 @@ metadata: namespace: acme-reviews spec: doc: Keeping buyer confidence high after a sale closes, including seller reputation. + project: + id: + namespace: acme-reviews + slug: acme-reviews + version: '1' title: Customer Trust diff --git a/examples/manifests/acme-reviews/project.yaml b/examples/manifests/acme-reviews/project.yaml new file mode 100644 index 00000000..aed8b5ce --- /dev/null +++ b/examples/manifests/acme-reviews/project.yaml @@ -0,0 +1,8 @@ +apiVersion: eventmodel.atlas.trogonstack.com/v1alpha1 +kind: Project +metadata: + name: acme-reviews + namespace: acme-reviews +spec: + doc: Review eligibility and submission for delivered storefront orders. + title: Acme Reviews diff --git a/examples/manifests/acme-reviews/read-model-slice.yaml b/examples/manifests/acme-reviews/read-model-slice.yaml index 6bcf4f81..0e5a006f 100644 --- a/examples/manifests/acme-reviews/read-model-slice.yaml +++ b/examples/manifests/acme-reviews/read-model-slice.yaml @@ -1,27 +1,5 @@ apiVersion: eventmodel.atlas.trogonstack.com/v1alpha1 kind: ReadModelSlice -metadata: - name: orders-delivered-unreviewed-upsert-rms - namespace: acme-reviews -spec: - doc: A delivered order appears on the queue. - projectionRole: PROJECTION_ROLE_UPSERT - readModel: - readModel: - id: - namespace: acme-reviews - slug: orders.delivered-unreviewed - version: '1' - sourceEvents: - - event: - id: - namespace: acme-storefront - slug: order.fulfilled - version: '1' - title: 'Queue: delivered unreviewed' ---- -apiVersion: eventmodel.atlas.trogonstack.com/v1alpha1 -kind: ReadModelSlice metadata: name: orders-delivered-unreviewed-delete-rms namespace: acme-reviews diff --git a/examples/manifests/acme-storefront/domain.yaml b/examples/manifests/acme-storefront/domain.yaml index d6b0e8d0..11f381fc 100644 --- a/examples/manifests/acme-storefront/domain.yaml +++ b/examples/manifests/acme-storefront/domain.yaml @@ -5,4 +5,9 @@ metadata: namespace: acme-storefront spec: doc: Buying, paying for, and fulfilling physical goods sold through the Acme storefront. + project: + id: + namespace: acme-storefront + slug: acme-storefront + version: '1' title: Retail Commerce diff --git a/examples/manifests/acme-storefront/project.yaml b/examples/manifests/acme-storefront/project.yaml new file mode 100644 index 00000000..49fa0471 --- /dev/null +++ b/examples/manifests/acme-storefront/project.yaml @@ -0,0 +1,8 @@ +apiVersion: eventmodel.atlas.trogonstack.com/v1alpha1 +kind: Project +metadata: + name: acme-storefront + namespace: acme-storefront +spec: + doc: The Acme storefront, from cart and checkout to payment and fulfillment. + title: Acme Storefront diff --git a/rsworkspace/crates/trogon-atlas-server/tests/example_manifests_validate.rs b/rsworkspace/crates/trogon-atlas-server/tests/example_manifests_validate.rs index ecaa13b4..a4732371 100644 --- a/rsworkspace/crates/trogon-atlas-server/tests/example_manifests_validate.rs +++ b/rsworkspace/crates/trogon-atlas-server/tests/example_manifests_validate.rs @@ -101,3 +101,35 @@ async fn example_models_raise_no_errors_or_warnings() { findings.join("\n") ); } + +#[tokio::test(flavor = "multi_thread")] +async fn example_projects_raise_no_errors_or_warnings() { + let svc = seed_all_examples().await; + let mut findings = Vec::new(); + for (ns, _) in NAMESPACES { + let resp = svc + .validate_project(Request::new(pb::ValidateProjectRequest { + scope: Some(pb::validate_project_request::Scope::ProjectId(pb::Id { + namespace: ns.into(), + slug: ns.into(), + version: 1, + })), + })) + .await + .unwrap() + .into_inner(); + assert!(!resp.reports.is_empty(), "{ns} project reported no models"); + for report in resp.reports { + for i in report.issues { + if i.severity >= pb::validation_issue::Severity::Warning as i32 { + findings.push(format!("{ns} {} {:?} :: {}", i.code, i.subject, i.message)); + } + } + } + } + assert!( + findings.is_empty(), + "example projects must validate cleanly:\n{}", + findings.join("\n") + ); +}