Skip to content

Contracts

Cratis.Showrunner.Notes.Contracts holds the public events an application and the receiving side exchange about notes. An application gets them through Cratis.Showrunner.Notes, which publishes the application’s side and translates the receiving side’s answers. The builder of the application gets them through Cratis.Showrunner.Notes.Collecting, which receives the notes and publishes the answers. Either side can also reference the contracts package on its own.

Notes records every fact about a note as a private event in the application, in the tenant’s own namespace. The private events name the actor (GiverGaveNoteOnContext, ReceiverAddressedNote). The public events in this package name what happened to the note (NoteOnContextGiven, NoteAddressed). The mapping at the end of this page lists every pair.

  • The note id is the event source id. Every event, in both directions, is appended under the note’s NoteId: a GUID in its lowercase, hyphenated D form. The id is event context, not a field in the payload. The receiving side answers under the same id it received.
  • Every payload carries TenantId. It is the name of the tenant’s Chronicle namespace in the application. The receiving side serves many applications and tenants, records each note in the namespace that TenantId names, and publishes its answer in that namespace so the application routes it back to the tenant.
  • Nothing is nullable. An optional collection is empty, and a value that is not known is the concept’s NotSet, an empty string. No dialog is NoteDialog.NoDialog; a screen that could not be resolved is NoteScreen.NotSet.
  • No timestamps that repeat the event’s own time. When something happened is the event’s Occurred in its context.
  • Every event is [Public], and none names an origin store. There is no [EventStore] attribute on any contract or on the assembly, because store names vary by deployment. Both sides subscribe explicitly; see Subscribe to the other side.
  • The giver is the compliance subject. Every fact a user records is appended with the subject notes-giver-<sha-256 of the user id>, and the publisher publishes each contract event with the subject of the private event it comes from. One erasure of that subject therefore covers everything the person wrote on all of their notes. The subject is a one-way hash because it travels unencrypted with every event.

Namespaces are relative to Cratis.Showrunner.Notes.Contracts; TenantId and NoteId sit in the root namespace. SliceLocation, ApplicationName, ApplicationVersion and CorrelationId are Usher’s, in Cratis.Showrunner.Usher.Contracts.Reporting: Notes reuses them rather than defining a second place in the application, so both parts describe a place and an application the same way. This is a project reference, so referencing the Notes contracts brings in the Usher contracts package; the Usher events are not used unless the host adds Usher.

The application publishes these from its outbox.

EventNamespacePayload
NoteOnContextGivenGivingTenantId, Application: PublishedApplication, Giver: NoteGiver, Context: NoteContext, Kind: NoteKind, Text: NoteText
NoteOnSlicesGivenGivingTenantId, Application, Giver, Context, Slices: IEnumerable<SliceLocation>, Kind, Text
NoteOnScreenGivenGivingTenantId, Application, Giver, Context, Kind, Text
NoteOnComponentGivenGivingTenantId, Application, Giver, Context, Component: PickedComponent, Kind, Text
ComponentPropertyChangeSuggestedComponentsTenantId, Application, Giver, Context, Component, Property: ComponentProperty, CurrentValue: PropertyValue, SuggestedValue: PropertyValue
NoteWithdrawnGivingTenantId
  • Each is a distinct fact. NoteOnContextGiven is the default action of the feedback button; NoteOnSlicesGiven is for slices the user explicitly picked; NoteOnScreenGiven is for the screen picked as the subject, as opposed to the context it happens to be in.
  • NoteOnComponentGiven and the ComponentPropertyChangeSuggested events of the same picking session share the NoteId. There is one ComponentPropertyChangeSuggested event per property, and a session can suggest changes without writing text.
  • NoteWithdrawn ends the note: the receiving side stops acting on it.

The receiving side publishes these from its outbox, so the giver can see what happened.

EventNamespacePayload
NoteAcknowledgedResponsesTenantId
NoteAddressedResponsesTenantId, AddressedInVersion
NoteDeclinedResponsesTenantId, Reason: DeclineReason

NoteAcknowledged says the receiving side recorded the note, NoteAddressed that a version that acts on it was rolled out, and NoteDeclined that it decided not to act on it.

TypeNamespaceShape
NoteGiverPeopleUserId: GiverUserId ([PII]), DisplayName: GiverDisplayName ([PII])
PublishedApplicationApplicationsApplicationName, ApplicationVersion, PublicationId. The PublicationId is how the receiving side finds the organization, application and event model a note is about.
NoteContextContextLocation: SliceLocation, ActiveSlices: IEnumerable<SliceLocation>, Screen: NoteScreen, Route, Dialog: NoteDialog, ModelReferences: IDictionary<string, string>, CorrelationId
NoteScreenContextScreenId, ScreenName, ScreenPath. NoteScreen.NotSet has every part empty.
NoteDialogContextDialogTitle, Location: SliceLocation. NoteDialog.NoDialog has an empty title and SliceLocation.NotSet.
PickedComponentComponentsComponentName, ComponentId, Part: ComponentPart, Ancestors: IEnumerable<ComponentName>, Location: SliceLocation
NoteKindGivingProblem = 0, Suggestion = 1, Question = 2, Praise = 3
NoteTextGivingText the user wrote ([PII])

ModelReferences holds identifiers of model elements, supplied by the host and keyed by what they identify, such as the event model or the slice of the model the screen renders.

What the user writes, and who they are, is classified [PII] on the shared concepts themselves, so it is encrypted in both stores: NoteText, GiverUserId and GiverDisplayName. The DeclineReason and AddressedInVersion the receiving side writes are the builder’s own words and are not classified.

The receiving side reads forwarded events in the clear. Encryption keys are per event store and namespace. When Chronicle forwards an event from the application’s outbox into the receiving side’s inbox, it copies the subject’s key into the receiving store, in the same namespace, the first time it forwards an event of that subject. Erasure is per store: erasing the giver in the application deletes the application’s key, and a receiving side that must forget them too deletes the same subject in its own store.

Nothing in the contracts package routes an observer to the other side. Each side keeps the name of the other side’s store in its own configuration and does two things at startup:

  1. Subscribe its own store to the other store’s outbox. Chronicle forwards the listed event types into inbox-{other store} on the subscribing store, in the same namespace. Use a subscription id of the form notes-{other store} and filter the event types its consumers need.
  2. Observe inbox-{other store} with observers that name that event sequence.

An application gets this from Cratis.Showrunner.Notes: it subscribes to each store in CollectingStores for the three answers. The receiving side gets it from Cratis.Showrunner.Notes.Collecting: it subscribes to each application store for the six notes. Consumers on both sides must tolerate the same event arriving twice, because the forwarder can deliver it again. See Collecting for what the receiving system has to configure.

The publisher adds TenantId to every public event: the namespace the private fact was recorded in.

Private event in the applicationPublic event to the receiving side
GiverGaveNoteOnContextNoteOnContextGiven
GiverGaveNoteOnSlicesNoteOnSlicesGiven
GiverGaveNoteOnScreenNoteOnScreenGiven
GiverGaveNoteOnComponentNoteOnComponentGiven
GiverSuggestedComponentPropertyChangeComponentPropertyChangeSuggested
GiverWithdrewNoteNoteWithdrawn

The application’s translator appends each answer under the same note id, in the namespace that TenantId names. It ignores a note it did not record, and a redelivered event changes nothing.

Public event from the receiving sidePrivate event in the application
NoteAcknowledgedReceiverAcknowledgedNote
NoteAddressedReceiverAddressedNote
NoteDeclinedReceiverDeclinedNote

The collecting side records the notes the same way:

Public event from the applicationPrivate event on the collecting side
NoteOnContextGivenNoteOnContextReceived
NoteOnSlicesGivenNoteOnSlicesReceived
NoteOnScreenGivenNoteOnScreenReceived
NoteOnComponentGivenNoteOnComponentReceived
ComponentPropertyChangeSuggestedComponentPropertyChangeReceived
NoteWithdrawnNoteWithdrawalReceived

and publishes the builder’s answers: OperatorAcknowledgedNote as NoteAcknowledged, OperatorAddressedNote as NoteAddressed and OperatorDeclinedNote as NoteDeclined.

Every event is guarded by EventSchemas.snapshot.json and ReleasedEventSchemas.json in the contracts project. The specification for_EventSchemas fails when a released schema changes, and when the snapshot is out of date for an unreleased one. Regenerate the snapshot with NOTES_UPDATE_EVENT_SCHEMA_SNAPSHOT=1 dotnet test Source/DotNET/Notes.Contracts/Notes.Contracts.csproj --filter FullyQualifiedName~and_comparing_the_snapshot, review the difference, and add the keys of events that were released to ReleasedEventSchemas.json. A released event is never changed; a new generation is added instead.