Skip to content

CHR0043: Key redirection carries a [PII] value across the compliance subject

A projection redirects the resolved document key and carries a [PII] value onto that document.

A read model document holds exactly one stored compliance subject. The kernel uses an explicit persisted event subject when it differs from the event source id; otherwise it uses the resolved document key. A root UsingKey, UsingKeyFromContext, composite key, or constant key changes that document key. In a child scope, UsingKey only identifies the child; UsingParentKey, UsingParentKeyFromContext, a parent composite key, or a constant parent key routes the containing document. The model-bound equivalents are [FromEvent] and [ChildrenFrom].

Declaring [Subject] on the event type is not a suppression. Its runtime value can be null or empty, append metadata can override it, and already-stored historical events retain the subject metadata they were appended with. EventSourceId selects the stream; it is the default subject only when no different explicit subject was persisted. The analyzer recognizes the fluent controls that key directly from EventContext.Subject or EventContext.EventSourceId, because either choice stays aligned with the kernel’s explicit-subject-or-resolved-key rule.

PII reach is recursive. A mapped event member counts when [PII] occurs directly, on its type, in an inherited member, or inside a nested value object, array, or collection element. The fluent analyzer follows the real public mapping surface: nested expression paths, Set/To, Add/Subtract, AddChild, SetThisValue, PropertyPath Set/To, and the final AutoMap state.

Nothing has to fail at read time. The cost can remain hidden as erasure reach: crypto-shredding the owner’s key does not touch a copy resting under a different subject, so the value survives an erasure that reports success.

Keep the personal value on a read model keyed and subjected by its owner, or resolve it at the query edge under the owner-scoped read model. Do not move it onto a request-, case-, tenant-, or other subject’s document merely to make a query convenient.

using System;
using Cratis.Chronicle.Compliance.GDPR;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections;
[EventType]
public record Chr0043AdvisorNamed(Guid RequestId, [property: PII] string FullName);
public record Chr0043RequestSummary([property: Key] Guid Id, string AdvisorName);
public class Chr0043RequestSummaryProjection : IProjectionFor<Chr0043RequestSummary>
{
public void Define(IProjectionBuilderFor<Chr0043RequestSummary> builder) => builder
.From<Chr0043AdvisorNamed>(_ => _
// Warning CHR0043: the resolved document is routed through RequestId while
// FullName belongs to the persisted event subject, which may be another identity.
.UsingKey(e => e.RequestId)
.Set(m => m.AdvisorName).To(e => e.FullName));
}

Fixing the projection prevents future wrong-subject materialization; it does not remove or re-encrypt copies already stored under the wrong subject. Before claiming erasure coverage, replace the affected read-model storage by dropping and rebuilding the projection into a clean container, rebuilding into a new container and switching readers, or performing an audited migration that proves the owner of every personal value and rewrites or removes each wrong-subject document. Replaying corrected code over the existing container is not sufficient evidence that stale copies are gone.

The default severity is warning. This is a rollout signal, not a compatibility guarantee or an erasure grace period: repositories that treat warnings as errors can fail immediately, and affected persisted read models still require the cleanup above. Any later promotion to error is a separate compatibility decision; suppressing or retaining warning severity does not make the storage shape safe.

Suppress with #pragma warning disable CHR0043 around the declaration, or set dotnet_diagnostic.CHR0043.severity = none in .editorconfig. Suppressing leaves the value outside its owner’s erasure — the defect is not decryptability, so nothing at runtime will report it.