Skip to content

Say that a readmodel and a reducer render to nothing - #26

Merged
woksin merged 2 commits into
mainfrom
fix/unrendered-readmodel-reducer
Aug 11, 2026
Merged

Say that a readmodel and a reducer render to nothing#26
woksin merged 2 commits into
mainfrom
fix/unrendered-readmodel-reducer

Conversation

@woksin

@woksin woksin commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixed

  • Rendering now reports a readmodel and a reducer it does not render, instead of dropping them silently. A slice declaring either previously rendered an application that looked complete while nothing held or populated the state they declare.

UnrenderedConstructs reported nine construct families and neither of
these two, so a slice declaring them rendered an application that looks
complete while nothing holds or populates the state they declare - the
exact indistinguishability the file's own contract says it exists to
prevent.

A declared readmodel is counted the way projections already are: the
rendered read model is inferred from the slice's first projection, so at
most one declared readmodel has a rendered counterpart. Nothing renders
a reducer at all, so every one is reported.
@woksin woksin added the patch label Aug 11, 2026
@woksin

woksin commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer context — not release notes.

What changed

UnrenderedConstructs.Families() yielded nine families and neither readmodel nor reducer, which contradicted the file's own stated contract at :15-19"A construct that renders to nothing and says nothing is indistinguishable from one that was never declared."

The spec that pinned the count of 9 is updated to 11, plus a should_report_* fact per new family. No other spec moved: Rendering.Cratis went 211 → 213, all green.

Counting rule for readmodel — mirrored from how projection is already counted, rather than reporting every declaration. StateViewSliceRenderer infers the rendered read model from the slice's first projection (projection.ReadModel ?? projection.Name), so at most one declared readmodel has a rendered counterpart and the rest have none. A slice that declares no readmodel at all yields -1, which the existing Where(family => family.Count > 0) filters out.

reducer has no RenderedConstructs flag and no renderer, so every one is reported — same shape as constraint, screen, capture and specification.

No issue reference in the body. Nothing in the tracker covers these two families — #13 names authorize, query and constraint, which UnrenderedConstructs already reports. Per .ai/rules/pull-requests.md the reference is omitted rather than guessed at. The observation was raised in the premise-correction comment on #23 if it is worth filing.

Verified, not fixed — worth its own PR

StateViewSliceRenderer.cs:109-110 widens read-model authorization across projections. Confirmed:

var authorization = AuthorizationRenderer.Render(
    queries.Select(query => query.Authorize), applicationSet, $"Read model '{typeName}'", diagnostics);

queries is every query in the slice, with no filter on which read model each one targets. Two consequences on a multi-projection slice:

  1. Only the first projection renders a read model, but the queries belonging to the dropped projections still contribute their authorize to the surviving one — the rendered read model is guarded by the union of authorizations for models that were never rendered.
  2. AuthorizationRenderer.Render :71-74 returns AllowAnonymous when any entry is unauthorized:
if (declared.Length == 0 || declared.Any(authorize => authorize?.References().Any() != true))
{
    return "AllowAnonymous";
}

So one unguarded query anywhere in the slice collapses the whole read model to [AllowAnonymous] — including when that query belonged to a projection that was dropped.

My read: yes, worth its own PR, and it is a security-shaped bug rather than a fidelity nicety — the failure direction is more permissive than the document. It is also independent of Stage#20: #20 is about and rendering as either within one authorize, this is about whose authorize is consulted at all. The narrow fix is to pass only the queries whose ReturnType.Name matches the rendered typeName, and to report the queries excluded that way. I did not fold it in here — this PR is a diagnostics-only change with no rendered-output difference, and that fix changes emitted attributes.

StageQueryPerformer.cs:60,66 hard-codes AllowsAnonymousAccess => true and IsAuthorized => true. Moot today because Perform returns null/empty, so nothing is actually exposed — but it is a real hole the moment the performer starts returning data, and it is already named in #13's body. I would leave it inside #13 rather than open a duplicate.

Verification

dotnet build -c Debug    → 6 projects, 0 errors, 0 warnings
dotnet build -c Release  → 6 projects, 0 errors, 0 warnings
dotnet test  -c Debug    → Contracts 90, Stage 19, Rendering.Cratis 213 — 0 failed

Stacked on #25 (base is fix/release-build-roslynator), which restores the Release build. GitHub retargets this to main when #25 merges.

@woksin
woksin changed the base branch from fix/release-build-roslynator to main August 11, 2026 23:14
@woksin
woksin merged commit 093212f into main Aug 11, 2026
1 check passed
@woksin
woksin deleted the fix/unrendered-readmodel-reducer branch August 11, 2026 23:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant