Skip to content

Epic: Internal read-model mutations and portable projectors - #166

Merged
patrickleet merged 22 commits into
tasks--graphql-qs-epicfrom
tasks--graphql-qs-mutation-projectors-1
Jul 31, 2026
Merged

Epic: Internal read-model mutations and portable projectors#166
patrickleet merged 22 commits into
tasks--graphql-qs-epicfrom
tasks--graphql-qs-mutation-projectors-1

Conversation

@patrickleet

@patrickleet patrickleet commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements [[tasks/graphql-qs-mutation-projectors-1]] — the mutation-projector authoring model from specs/query-layer/v1/domain-event-projections.

Delivered work packages

  • WP1: Versioned event-independent MutationProgram IR, field-presence lattice, sugar≡explicit, golden fixture tests/fixtures/mutation-program-v1.json, mutation! macro, typed ReadModelMutationCapabilities (not GraphQL fields).
  • WP2/WP3: Server interpreter (mutation rewrite → projection ops) and role-safe cache lowering (Rust + shipped JS lowerMutationCache) with auth/absent-base fail-closed vectors.
  • WP4/WP5: Portable handler catalog (owner/event/epoch uniqueness) and preview composition with causal scope retention.
  • WP6–WP9: SAVE_TODO/DELETE_TODO, SAVE_CHAT_MESSAGE, multi-model relationship fixture, SAVE_BLOB_GAME; client call shapes unchanged (await commands.todo.complete({ todo_id })).
  • WP10 / AC4 cutover (hard delete):
    • Event-owning projection! proc-macro removed
    • Command-side .project(...) selectors removed; Blob uses placement-selected commit()?.projected()
    • Public ProjectionReadModelWorkspace / graph load builder no longer exported (crate-private)
    • Separately authored command_effects! / command_confirmations! macros removed
    • TypedCommand::effects / ::confirmations builders removed; commands use .emits / .preview + mutation IR
    • Docs (e2e-ui README, UI walkthrough, README, GraphQL skill) teach mutation! path
    • Structural gate: tests/legacy_authoring_absence.rs

Retained low-level adapters (explicitly allowed)

  • TableWritePlan / repository traits
  • distributed::read_model::ReadModelWritePlanBuilder (not crate-root authoring)
  • command_input_defaults! (input generators, not effect authoring)

Base

  • Integration base: tasks--graphql-qs-epic @ abe05e3e0d2e35beb13db96dc4e221481f3cab37
  • Head: tasks--graphql-qs-mutation-projectors-1

Test plan

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace --all-targets --all-features
  • npm --prefix js run quality
  • make -C tests/e2e-ui check-client
  • make -C tests/e2e-ui test (offline)
  • Structural: legacy_authoring_absence + inventory empty for forbidden authoring surfaces
  • Live browser Playwright / Postgres / OIDC matrix — environment-dependent; offline gates green; live blocked when stack not up (honest env boundary)

Notes

Does not merge to main, publish, or deploy. PR base is intentionally tasks--graphql-qs-epic, not main.

Introduce the public mutation authoring path from the domain-event-projections
spec: versioned MutationProgram IR, mutation! macro, ReadModel capabilities
metadata, portable handler catalog, server/cache/preview interpreters, and
shared Rust/JS golden vectors. Wire SAVE_TODO/DELETE_TODO, SAVE_CHAT_MESSAGE,
and SAVE_BLOB_GAME mutations into e2e fixtures while retaining projection!
as the dual-path runtime mount until full cutover.

Implements [[tasks/graphql-qs-mutation-projectors-1]]
Implements [[tasks/graphql-qs-mutation-projectors-2]]
Implements [[tasks/graphql-qs-mutation-projectors-3]]
Implements [[tasks/graphql-qs-mutation-projectors-4]]
Implements [[tasks/graphql-qs-mutation-projectors-5]]
Implements [[tasks/graphql-qs-mutation-projectors-6]]
Implements [[tasks/graphql-qs-mutation-projectors-7]]
Implements [[tasks/graphql-qs-mutation-projectors-8]]
Implements [[tasks/graphql-qs-mutation-projectors-9]]
Implements [[tasks/graphql-qs-mutation-projectors-10]]
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dce52b52-ee68-40ab-b59d-df6f963259d9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…rvice path

- Add placement-selected direct registry so commands call commit()?.projected()
  without naming a projection selector (Blob handlers updated).
- Register BlobGames executor at e2e service construction.
- Drive SAVE_TODO/DELETE_TODO/SAVE_CHAT_MESSAGE/SAVE_BLOB_GAME mutation
  programs from service registration and event handlers (not test-only stubs).
- Hide event-owning projection! authoring from public docs; document
  .project(...) as non-preferred for application commands.
- Unit proof: placement_selected_projected_uses_registered_executor_without_project_selector

Implements [[tasks/graphql-qs-mutation-projectors-1]]
Implements [[tasks/graphql-qs-mutation-projectors-10]]
Implements [[tasks/graphql-qs-mutation-projectors-11]]
Replace event-owning projection! for TODO/CHAT/BLOB e2e mounts with
mutation-backed ProjectionDescriptor factories:

- program/resolve built via program_from_mutation_arms (SAVE_*/DELETE_*)
- lower via shared lower_single_model ORM path
- service construction asserts descriptor program bytes == mutation rewrite
- handlers apply those descriptors without id()-only theater
- Blob eligibility fixtures still use projection! for compile_fail only

Also expose ResolvedProjectionPlan::resolve publicly for dual-path factories.

Implements [[tasks/graphql-qs-mutation-projectors-1]]
Hard-delete competing projector authoring surfaces:

- Remove event-owning `projection!` proc-macro export and source module
- Delete distributed_macros projection unit/compile_fail suites
- Remove CausalCommitBuilder/CausalRepository `.project(...)` selectors
- Drop crate-root `ReadModelWritePlanBuilder` re-export (adapter remains at
  `distributed::read_model::ReadModelWritePlanBuilder` and TableWritePlan)
- Migrate residual framework/test descriptors to mutation-backed factories
- Add tests/legacy_authoring_absence.rs structural gate

Task-11 matrix (fmt, clippy -D, workspace tests, npm quality, e2e-ui
check-client/test) green at this cutover; browser/Postgres live env gates
remain environment-dependent.

Implements [[tasks/graphql-qs-mutation-projectors-1]]
Implements [[tasks/graphql-qs-mutation-projectors-11]]
…or ORM export

Hard-delete separately authored command_effects!/command_confirmations! and
TypedCommand::{effects,confirmations}; demote ProjectionReadModelWorkspace to
crate-private; docs teach mutation! + .emits/.preview; expand structural gate;
restore command_input_defaults trybuild suite under a dedicated name.

Full task-11 matrix green (fmt, clippy -D, cargo test workspace all-features,
npm quality, e2e-ui offline, legacy_authoring_absence).

Implements [[tasks/graphql-qs-mutation-projectors-1]]
Implements [[tasks/graphql-qs-mutation-projectors-11]]
Replace residual repo.project(BLOB_GAMES).commit(...).projected() examples
in e2e-ui README and walkthrough with the live
repo.commit(game)?.projected() path. Keep compile_fail/migration notes only
for the removed selector.

Implements [[tasks/graphql-qs-mutation-projectors-1]]
Implements [[tasks/graphql-qs-mutation-projectors-11]]
Delete the crate-private graph_workspace ORM path, strip dead command_effects
authoring types/re-exports and constructors, rename macros module to
command_input_defaults, and tighten structural gates. Keep protocol lifecycle
primitives on CausalProjectorContext and low-level TableWritePlan adapters.

Full matrix green (fmt, clippy -D, cargo test workspace, npm quality, e2e-ui).

Implements [[tasks/graphql-qs-mutation-projectors-1]]
Implements [[tasks/graphql-qs-mutation-projectors-11]]
@patrickleet

Copy link
Copy Markdown
Collaborator Author

Release-ready cleanup landed @ 7ad8d5b960acf03c2681049e4b66ce70dfa47155

  • Deleted graph_workspace multi-table ORM path entirely
  • Removed dead effects authoring re-exports (CompiledCommandEffects, confirmations constructors, upsert/patch helpers)
  • Renamed macros module to command_input_defaults
  • Structural gate expanded (7 tests)
  • Full task-11 matrix green (see CI + local release-cleanup-matrix.log)

Still intentionally retained: low-level TableWritePlan adapters, command_input_defaults!, protocol lifecycle on CausalProjectorContext (load/project/delete for recovery/protocol tests), derive-owned key markers.

Framework owns resolve/lower/inventory factory glue via mutation_projector!
and arm_state_upsert_for_model / arms_state_upsert_for_model /
arm_delete_pk_from_envelope / build_mutation_projector_program.

e2e todos/chat/blob projections shrink to mutations + event arms + mount.

Implements [[tasks/graphql-qs-mutation-projectors-1]]
Implements [[tasks/graphql-qs-mutation-projectors-1]]
App authors declare mutations and which events apply them. Dual-path
projection IR compile stays internal (compile_portable_handlers).

Rename public bind_* helpers; hide arm_* vocabulary from the crate root
docs. Rewrite e2e todos/chat/blob to portable_handlers! / bind language.

Implements [[tasks/graphql-qs-mutation-projectors-1]]
…ers only

Delete arm_* / build_mutation_projector_program shims and MutationProjectionArm.
Public API is mutations + bind_* + portable_handlers! / compile_portable_handlers.
Rename opaque compile unit to PortableHandler. Strip e2e program wrappers and
service dual-path equality asserts.

Implements [[tasks/graphql-qs-mutation-projectors-1]]
Matches BLOB_GAMES and CHAT_MESSAGES — the mount is the Todos model
handlers, not a "reads" collection.

Implements [[tasks/graphql-qs-mutation-projectors-1]]
…emos

Inline todo.complete preview with the other commands via state_preview! on
the typed command. Remove complete_preview from projections. Align event
handler comments and UI walkthrough with mutation + portable_handlers.

Implements [[tasks/graphql-qs-mutation-projectors-1]]
… commits

Ship the revised domain-event projection authoring model for the mutation
projectors cutover:

- GraphQL-looking mutation documents via mutation_file! / mutation! (syntax-only IR)
- Event-first portable_handlers! (on <events> apply <mutation>)
- Command .applies for known mutation-input mapping (preview kept as alias)
- Blob projected path: Mutation::from_state + readmodel(row).commit()?.projected()
- e2e command modules renamed with aggregate prefix (todo_reopen, …)

Implements [[tasks/graphql-qs-mutation-projectors-1]]
Live queries that join unowned tables (e.g. chat_messages.author → auth_users)
no longer force live.supported=false; index plans skip zero-projector join
tables so the WebSocket can stay active. Nullable missing relationship edges
stay complete so optimistic rows remain materializable, and the chat UI no
longer blanks the list while incomplete.

Also: application surface uses user grants only so Todos/BlobGames keep a
portable owner row policy for optimistic list inserts; chat unit partition for
lobby resume; iMessage-style Sent/Delivered footers and AuthUsers display
names on chat.

Implements follow-ups for [[tasks/graphql-qs-mutation-projectors-1]]
Same-scope soft-nav rehydrate merges route SSR seeds instead of wiping
confirmed records/indexes omitted from the page. Lobby chat uses a fixed
scroll panel, page size 25, infinite history, and Chromium-correct
column-reverse scroll geometry (negative scrollTop).
Gate causal_direct_v1_program behind the graphql feature so default/sqlite
lib tests compile, and send the exact e2e-ui application surface roles list
expected by select_protocol_surface.
Local Zitadel bootstrap and offline suite tokens assert both admin and
user for admin humans/machines so they can use normal app surfaces and
elevated paths without changing model permissions.
Command-substitution of create_human was capturing "reusing human …"
lines into E2E_HUMAN_*_UID. Keep only the user id on stdout.
Make multi-role principals first-class for named application surfaces:
eligible roles control protocol open (any asserted role may open), while
schema privilege roles shape the portable client contract. e2e-ui keeps
eligible {admin,user} with schema {user} so owner-portable optimism is
preserved without collapsing model permissions.

Also stop treating incomplete/empty history pages as end-of-history while
the live chat window is still filling, and harden the Playwright history
scroll test under column-reverse.

Implements multi-role surface selection for [[tasks/graphql-qs-mutation-projectors-1]]
Point the fixture source gate at surface_for_application_contract and
eligible {admin,user} schema {user}. Widen the chat revalidation
optimism paint window so CI headroom stays under the delayed mutation
response without weakening the stale-while-revalidate assertion.
@patrickleet
patrickleet merged commit 681e17e into tasks--graphql-qs-epic Jul 31, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant