Skip to content

v4.0.0

Choose a tag to compare

@github-actions github-actions released this 13 Aug 03:45
· 53 commits to main since this release

What's changed in v4.0.0

  • chore(deps): update actions/checkout action to v7 (by @renovate[bot])

  • chore(deps): update actions/upload-artifact action to v7 (by @renovate[bot])

  • chore(deps): update marocchino/sticky-pull-request-comment action to v3 (by @renovate[bot])

  • chore: atc gitignore (by @patrickleet)

  • feat: GraphQL query edge, client replica, and e2e-ui demos (#127) (by @patrickleet)

    BREAKING CHANGE: * feat(e2e-ui): polish demos, event domains, blob URL routes

    Teach-first home and cleaned UI across todos/chat/session/admin/blob.
    Todo/chat use public #[event] command methods; handlers stay thin.
    Blob moves to [[gameId]] with local board paint so New game updates
    immediately while history still uses the shared cache.

    • fix(e2e-ui): wait for move confirms before blob next level

    Optimistic level-complete painted the board before the finishing move
    committed, so start_level loaded an incomplete aggregate and rejected.
    Gate Next level on server-confirmed completion and await the move drain.

    • fix(e2e-ui): restore todo/chat event wire formats

    The #[event]+when= domain polish changed todo.completed/reopened/archived
    payloads (empty → owner_id) and renamed (title → owner_id,title). Existing
    streams failed hydrate, so load/command paths looked like nothing saved.
    Restore Result + record_* command/event pairs with the original payloads;
    handlers match again. Chat restored for the same hard-error pattern.

    • fix(e2e-ui): drop fake board from blob empty state

    Show only copy + Start game when no game is selected, so the empty
    state does not look like a broken half-rendered grid.

    • test(e2e-ui): add Playwright browser e2e suite

    Chromium flows against the live Fieldnote UI: OIDC Login V2 as alice/admin,
    todos lifecycle, chat post, blob start/move, session, unauth redirects, and
    role-gated admin. Run with make up + make run, then make test-browser.

    • ci: run e2e-ui offline suite and Playwright on PR/main

    Add integration-e2e-ui workflow (make test + Docker stack + browser e2e)
    and wire it into on-pr-quality and on-push-main so Fieldnote fixture
    regressions gate merges and releases.

    • fix: green CI for e2e-ui HTTP surface, m2m where, blob start

    Disable public HTTP command wildcards (T0 404) while mounting Zitadel
    ingress/scrape explicitly. Include shadow/through tables in GraphQL
    surface so m2m relationship predicates appear on bool_exp. Survive
    blob /blob → /blob/{id} remount via remembered rows and list-seed merge.

    • fix(e2e-ui): hydrate-safe blob start + align offline UI contracts

    Wait for client hydration before Start/New game, paint from mutation
    payload without remount-racing goto, and assert blob_games_start over
    the network in Playwright. Update api-contract tests for the current
    home story and generated command policies re-export.

    • feat: extract Distributed JS client package

    Implements [[tasks/distributed-js-client-1]]

    • feat: select normalized cache engine

    Implements [[tasks/graphql-qs-client-replica-2]]

    • feat: emit authorized client manifests

    • chore: refresh e2e GraphQL artifacts

    • chore: refresh e2e command artifacts

    • feat: add normalized client replica

    • feat: add typed causal command contracts

    Implements tasks/graphql-qs-client-replica-4.

    • feat: add durable causal command dispatch

    Implements [[tasks/graphql-qs-client-replica-5]]

    • feat: harden causal projection protocol

    Implements tasks/graphql-qs-client-replica-15.

    • feat: expose causal GraphQL client protocol

    • feat: add unified client compiler foundation

    • feat: harden client compiler contracts

    • feat: compile recursive client replica plans

    Implements [[tasks/graphql-qs-client-replica-6]]

    • feat: complete compiler-backed client plans

    • docs: move durable decisions to distributed kb

    • feat: bind clients to authorization surfaces

    Implements the server and compiler slice of [[tasks/graphql-qs-client-replica-10]].

    • fix: keep manifest validation test current

    Covers [[tasks/graphql-qs-client-replica-10]].

    • feat!: complete compiler-backed client replica

    Generate role-scoped query and command artifacts from Rust Surface IR, make the normalized causal replica the only public client runtime, and migrate the SvelteKit e2e consumer to the generated contract.

    BREAKING CHANGE: remove the pre-release document cache, manual target/reconcile APIs, raw GraphQL command catalog, and pilot persistence format.

    • fix: isolate GraphiQL introspection limits

    • fix!: split query authority from causal comparability

    • fix: retain projection poll liveness

    • fix: preserve stale replica views during revalidation

    Implements [[tasks/graphql-qs-client-replica-18]]

    • fix: stabilize optimistic command feedback

    • fix: keep todo command controls visually stable

    • fix: preserve optimistic order while revalidating

    • fix: fence stale revalidation after projected commands

    • fix: retain projected rows until read-model echo

    • feat: add direct-only projection owners

    Implements [[tasks/graphql-qs-client-replica-20]].

    • fix(cli): treat exact --documents paths as literal files (#136)

    Skip glob expansion when a --documents value is an existing file so path
    segments like SvelteKit [[gameId]] are not interpreted as character classes.
    Globs continue to expand as before for patterns that are not existing files.

    • fix: enforce asserted roles in strict OIDC mode

    Implements [[tasks--graphql-qs-epic]]

    • fix: share expiring OIDC JWKS cache per engine

    Implements [[tasks--graphql-qs-epic]]

    • test: cover exact GraphQL document paths

    Implements [[tasks--graphql-qs-epic]]

    • fix: address GraphQL query service review findings

    Implements [[tasks--graphql-qs-epic]]

    • fix: keep projected fences against stale high-revision snapshots

    Conflicting GraphQL snapshot bodies must not override a direct projection
    even when response evidence is stamped later than the command. That race
    was rolling back live e2e blob moves after held revalidation responses.

    • fix: fence projected rows by response start

    Implements [[tasks--graphql-qs-epic]]

    • docs: position Distributed as full-stack CQRS + GraphQL + JS

    Expand the README opening, design goals, GraphQL section, e2e-ui template
    table (blob, OIDC, Projected), and @hops-ops/distributed client surface so the
    docs match the platform scope of the query-service epic.

    • docs: lead README with Fieldnote, Blob, and live demos

    Open with runnable full-stack examples (e2e-ui, GraphiQL, capability map)
    before dependency wiring and the library quick start.

    • docs: highlight first-class OIDC and multi-IdP e2e

    Call out built-in OidcBearer on the GraphQL edge and the live Zitadel,
    Keycloak, and Authentik compose/test suites in the README demos and identity
    section.

    • docs: lead demos with e2e-ui code index

    Point root README at the Fieldnote files that make SSR, live chat,
    Projected blob, dual client surfaces, and OIDC feel product-grade.
    Rewrite tests/e2e-ui/README as the deeper code map plus runbook.

    • docs(e2e-ui): refresh home walkthrough for latest patterns

    Update the Fieldnote index route code tour: Fact vs Projected,
    dual client surfaces, CausalCommandContext, real RLS/effects
    samples, and admin as a separate generated client.

    • refactor(e2e-ui): nest CSS natively; keep postcss for custom-media

    Nest app.css and route/component styles with native nesting (&, &-,
    nested @media). Leave nesting-rules off in postcss-preset-env so
    BEM &-suffix is not mis-expanded; keep custom-media and range queries.

    • revert(e2e-ui): undo CSS nesting rewrite

    Restores flat selectors that postcss/Vite were expanding incorrectly
    (&-suffix → -suffix.parent). Style architecture refactor should be
    component extraction + scoped Svelte CSS, not a global nest pass.

    • refactor(e2e-ui): product components + split global CSS

    Extract scoped product primitives (AppPage, PageHeader, InlineAlert,
    Panel, StatRow) and extend Button with ink/ghost/quiet/sm. Migrate
    todos/chat/blob/admin onto them. Split the 1.6k app.css into tokens
    (app.css), layout chrome (chrome.css), and home-only wireframe
    (home.css) so shared CSS is intentional and route/product UI can use
    Svelte scoping.

    • fix(e2e-ui): polish chat and blob after product shell migration

    Restore chat shell CSS variables and live-status states; tidy blob
    markup under AppPage.

    • fix(e2e-ui): read Zitadel project roles from access token

    Admin grants live on the access token (urn:zitadel:iam:org:project:roles),
    not the id_token. Session only decoded the id_token, so admin always became
    user. Merge groups from both tokens and align session/nav admin checks.

    • fix(e2e-ui): type-cast refreshed token when storing groups

    • fix(e2e-ui): always request Zitadel project role scopes at login

    Admin project role is granted in bootstrap, but tokens only include
    role claims when authorize requests the reserved roles scopes. Merge
    those scopes into every OIDC start (not only a complete OIDC_SCOPES
    env) and read roles from access + id tokens including project-id claims.

    • fix(e2e-ui): let admin use the normal fieldnote app surface

    fieldnote was registered for role user only, so sessions with concrete
    role admin failed protocol selection on todos/chat/blob. Register the
    shared surface for admin+user (admin still uses fieldnote-admin for
    elevated ops) and regenerate clients.

    • refactor: split distributed-replica into folder modules

    Move the monolithic replica implementation into package-private modules
    (types, constants, clocks, hydration, optimistic, helpers, watch, impl)
    while keeping createDistributedReplica on the same public export path.

    • fix(e2e-ui): align Playwright with product shell and admin hydration

    Todo specs still targeted removed fn-* classes after the product panel
    refactor. Update selectors, wait for fieldnote-admin hydration before
    force-archive, assert archive on the clicked row, and match optimistic
    order checks to create/reopen list behavior.

    • refactor: split command-runtime into folder modules

    Move the monolithic command runtime into package-private modules
    (symbols, types, errors, lifecycle, helpers, create) while keeping the
    same public export path for createReplicaCommandRuntime and related APIs.

    • refactor: split cache-engine into folder modules

    Move the private purpose-built cache engine into types, errors, helpers,
    engine, and create modules while preserving the internal import path for
    createCacheEngine, cacheIndexKey, and related types.

    • refactor: split query-plan into filter/order/pagination modules

    Move portable query-plan evaluation into focused modules (types, resolve,
    filter, order, pagination, util) while keeping the same public export
    path for evaluateReplicaFilter, compareReplicaOrder, and pagination.

    • refactor(js): extract shared helpers into src/lib

    Deduplicate deepEqual, reportSafely/reportUnhandled, isPlainRecord,
    compareCodeUnits, assertName, and freezeRecord behind js/src/lib so
    cache-engine, replica, and command-runtime share one implementation.

    • fix(js): clear unused imports so tsc check matches CI

    Module splits left type-only and value imports unused; npm test only
    runs build (no unusedLocals), while CI quality runs check --noEmit and
    failed. Strip unused imports so npm run quality is green.

    • refactor(js): split commands into types, prepare, presets, receipt

    Extract command artifact types, contract errors, and implementation so
    prepare/inventory/receipt concerns are separable. Public exports stay on
    ./commands.js. Implements [[tasks/js-less-context-7]].

    • refactor(js): split identity, helpers barrels, persistence, index, diagnostics
    • identity: keys/codec/clone barrels over implementation
    • command-runtime: concern barrels over helpers-impl
    • persistence, index-maintenance, diagnostics: folder + thin re-exports

    Implements [[tasks/js-less-context-8]] [[tasks/js-less-context-9]]
    [[tasks/js-less-context-10]] [[tasks/js-less-context-11]]
    [[tasks/js-less-context-12]].

    • refactor(js): extract distributed-replica impl concern modules

    Move fetch/live, protocol generation, optimistic layers, dehydrate/hydrate
    orchestration, and diagnostics emission into package-private helpers with
    thin class delegates. Implements [[tasks/js-less-context-2]] through
    [[tasks/js-less-context-6]].

    • refactor(js): body-split commands, identity, helpers, persistence, index, diagnostics

    Move function bodies into concern modules instead of thin re-export barrels so
    agents load only the needed concern. implementation.ts/helpers-impl.ts become
    re-export surfaces. Quality: 253 tests + check + pack:smoke + publint.

    Implements [[tasks/js-less-context-7]] [[tasks/js-less-context-8]]
    [[tasks/js-less-context-9]] [[tasks/js-less-context-10]]
    [[tasks/js-less-context-11]] [[tasks/js-less-context-12]]

    • refactor(js): move command-runtime helpers into lib/

    Bodies live under command-runtime/lib/{inventory,binding,effects,transport,
    status,output,projection,util}.ts. helpers.ts remains the stable barrel;
    helpers-impl.ts re-exports helpers.

    • refactor(js): drop command-runtime helpers.ts barrel

    Barrel lives at lib/index.ts; create.ts imports from ./lib/index.js.
    Removes helpers.ts and helpers-impl.ts naming leftover.

    • refactor: split graphql compile modules

    Implements [[tasks/rust-less-context-6]]

    • refactor: split client compiler manifest module

    Implements [[tasks/rust-less-context-3]]

    • refactor: split client compiler graphql pipeline

    Implements [[tasks/rust-less-context-2]]

    • refactor: split client compiler render module

    Implements [[tasks/rust-less-context-4]]

    • refactor: split client manifest module

    Implements [[tasks/rust-less-context-7]]

    • refactor: split command ledger module

    Implements [[tasks/rust-less-context-12]]

    • refactor: split sqlx repository core

    Implements [[tasks/rust-less-context-13]]

    • refactor: split command manifest compiler

    Implements [[tasks/rust-less-context-5]]

    • refactor: split graphql protocol helpers

    Implements [[tasks/rust-less-context-9]]

    • refactor: split GraphQL command contract modules

    Implements [[tasks/rust-less-context-8]]

    • fix(macros): update trybuild paths after command_contract split

    Compile-fail diagnostics now point at command_contract/effect_wire.rs
    (and sibling modules) instead of the old monolith path.

    • refactor: split projection protocol store module

    Implements [[tasks/rust-less-context-15]]

    • fix: re-export ProjectionPartitionSnapshot from store split

    The type lived in store/query.rs but was omitted from store/mod.rs
    pub(crate) re-exports, breaking graphql/sqlx consumers.

    • refactor: split projection protocol codec

    Implements [[tasks/rust-less-context-16]]

    • refactor: split hot rust modules

    Implements [[tasks/rust-less-context-23]]

    • refactor: split projector runtime module

    Implements [[tasks/rust-less-context-18]]

    • refactor: split sqlx read model module

    Implements [[tasks/rust-less-context-14]]

    • refactor: split distributed macros entry modules

    Implements [[tasks/rust-less-context-22]]

    • refactor: split graphql surface modules

    Implements [[tasks/rust-less-context-10]]

    • refactor: split graphql engine orchestration

    Implements [[tasks/rust-less-context-11]]

    • refactor: split microsvc service module

    Implements [[tasks/rust-less-context-17]]

    • refactor: folderize SQLx projection protocol

    Implements [[tasks/rust-less-context-19]]

    • refactor: folderize in-memory projection protocol

    Implements [[tasks/rust-less-context-20]]

    • refactor: share pure projection backend helpers

    Implements [[tasks/rust-less-context-21]]

    • refactor: share projection kind storage decoding

    Implements [[tasks/rust-dry-2]]

    • refactor: share projection digest helper

    Implements [[tasks/rust-dry-3]]

    • refactor: share projection failure batch predicate

    Implements [[tasks/rust-dry-4]]

    • refactor: share projection ownership validation

    Implements [[tasks/rust-dry-5]]

    • test: share projection protocol scenarios

    Implements [[tasks/rust-dry-6]]

    • refactor!: rename client GraphQL protocol version 2 → 1

    Never-released wire family; first public ship should be protocol v1, not v2.
    Constants, envelopes, compiler emit, client checks, fixtures, and docs only.
    Left unrelated versions alone (variableCodec v2, digest domain tags, package semver).

    • fix: update postgres/cli fingerprint goldens for protocol v1

    All-features and distributed_cli integration asserted stale schema
    fingerprints that include protocol_version in the hash input.

    • refactor!: ship unreleased wire versions as v1

    Normalize client manifest, variableCodec, command extension slots, and
    protocol-manifest epoch to 1 so the first public release does not imply
    prior public wire families. Refresh fingerprints, goldens, and fixtures.

    • fix: update orders harness schema fingerprint for v1 wire versions

    Ignored distributed_cli integration test expected the pre-v1 schema
    fingerprint; regenerate to match manifest/epoch/slot version renames.

    • fix: update postgres role-surface schema fingerprint for v1

    all-features client_surface_parity asserted a pre-v1 schema hash for the
    orders postgres role surface after wire version renames.

    • chore: drop NPM_RELEASE_READY gate from tag publish

    Bootstrap is complete; a permanent repo variable is unnecessary. Preflight
    still validates the tag form and that @hops-ops/distributed exists on npm.

    • fix(e2e-ui): drop dead Manage Account and Protected Page links

    Remove account-menu link to Zitadel console and session page button to
    /protected, which has no route in the template.

    • chore(e2e-ui): refresh generated clients after protocol v1 wire renames

    Schema fingerprints in the checked-in user/admin clients drifted after
    manifest/codec/slot versions shipped as v1. Keep gen-client outputs in
    lockstep for make check-client / CI drift gates.

    • chore: apply cargo fix for unused re-exports

    Drop unused imports/re-exports reported on default cargo build. Does not
    remove underlying types or methods still present for internal/store use.

    • chore: silence false dead_code noise without deleting protocol surface

    Default cargo build warns on helpers whose callers live behind graphql/sqlx
    or unit tests. Drop true unused re-exports/imports, restore feature-gated
    re-exports that cargo fix removed, and allow dead_code only on intentional
    store/protocol surface and test-oriented wrappers so product builds stay clean.

    • fix: restore test-only re-exports removed by cargo fix

    cargo fix / dead_code cleanup dropped re-exports that unit tests import
    (client_manifest_from_surface, ProjectionCheckpointProbe, obligation
    resolution types). Non-test lib builds still allow unused_imports so
    featureless cargo build stays quiet; lib tests compile again.

    • refactor(e2e-ui): rename Fieldnote demo to Todos

    Drop the invented product name. Surface IDs are todos / todos-admin so the
    demo is obviously a todos app for engineers, not a fictional brand.

    • refactor(e2e-ui): name template e2e-ui; keep Todos as one demo

    Surfaces are e2e-ui / e2e-ui-admin. Todos is only the /todos demo (alongside
    chat, blob, admin)—not the brand for the whole fixture.

    • feat: infer natural read-model storage names

    Implements [[tasks/graphql-qs-projection-model-5]]

    • refactor: rename successful command outcomes

    Implements [[tasks/graphql-qs-projection-model-2]]

    • feat(js): replace optimistic layers atomically

    Evaluate corrections against the target prefix, preserve layer metadata, and publish only the final rebased graph. Add typed missing-layer handling and private replica coverage.

    Implements [[tasks/graphql-qs-projection-model-14]]

    • feat: add typed domain-event occurrence runtime

    Implements [[tasks/graphql-qs-projection-model-3]]

    • feat: generate sourced domain-event capture

    Implements [[tasks/graphql-qs-projection-model-4]]

    • feat: define portable projection program IR

    Implements [[tasks/graphql-qs-projection-model-7]]

    • Implement fluent projection commit API

    Implements [[tasks/graphql-qs-projection-model-6]]

    • Implements [[tasks/graphql-qs-projection-model-8]]

    • Fix projection direct candidate classification

    • feat: add projection catalog identities and placement

    Implements [[tasks/graphql-qs-projection-model-9]]

    • test: align compile failures with succeeded outcomes

    • Implement causal projection executor and graph workspace

    Implements [[tasks/graphql-qs-projection-model-10]]

    • feat: declare exact command projection events

    • feat: bind modeled projections to GraphQL surfaces

    • feat: expose crate projection expression views

    • fix: authorize modeled projection surfaces

    • feat: type sourced domain event contracts

    • fix: seal adapter event contracts

    • fix: witness exact domain event bodies

    • test: refresh event contract diagnostics

    • feat: export strict authorized projection manifests

    • feat: model authorized optimistic projection occurrences

    • feat: define role-safe projection delta v1

    • chore: export projection delta module

    • fix: seal projection delta authority

    • fix: canonicalize projection delta final state

    • feat: lower sealed role-safe projection deltas

    • fix: harden projection delta recovery scopes

    • feat: expose role-safe projection partitions [[tasks/graphql-qs-projection-model-13]]

    • test: freeze projection delta v1 vectors

    Adds the cross-language canonical fixture, exact boundary coverage, authorization transition matrices, placement and provenance guards, relationship/delete cases, and manifest slot evidence.

    Implements [[tasks/graphql-qs-projection-model-13]]

    • feat: parse projection manifest v2 contracts

    Implements [[tasks/graphql-qs-projection-model-15]]

    • feat: compile projection preview artifacts

    Ports ProjectionDelta wire-v1 validation and canonical ordering, lowers command projection previews with conservative recovery, and emits fail-closed command artifact v2 without legacy effects or confirmations.

    [[tasks/graphql-qs-projection-model-15]]

    • test: lock projection compiler contracts

    Completes compiler validation, conservative relationship recovery, artifact v2 fixtures, and cross-layer regression coverage for projection programs, bindings, previews, and the frozen ProjectionDelta wire contract.

    [[tasks/graphql-qs-projection-model-15]]

    • fix: prove optimistic projection inputs

    Requires complete value-bearing upserts, preserves keyed relationship invalidation authority, and validates preview provenance against consistent opaque slot types with fail-closed numeric and shape semantics.

    [[tasks/graphql-qs-projection-model-15]]

    • fix: close projection preview safety gaps

    Separates absent values from clearing intent, proves scalar record keys, bounds expansion before allocation, narrows invalidation and revalidation scope, and emits an explicit compiler-owned artifact-v2 seam for Task 16.

    [[tasks/graphql-qs-projection-model-15]]

    • fix: fail closed at projection runtime seam

    Matches frozen composite-expression absence semantics, treats nested unset as non-destructive uncertainty, and omits artifact-v2 dispatch through the v1 JavaScript runtime while retaining typed inspectable artifacts.

    [[tasks/graphql-qs-projection-model-15]]

    • fix: fail closed on first-present unset

    Preserves frozen FirstPresent semantics by skipping only absent values, treating nested unset as non-destructive uncertainty, and proving record-scoped recovery without optimistic writes.

    [[tasks/graphql-qs-projection-model-15]]

    • Add authenticated projection metadata authority [[tasks/graphql-qs-projection-model-11]]

    • Persist modeled projection receipts exactly [[tasks/graphql-qs-projection-model-11]]

    • Mount modeled projectors and derive causal obligations [[tasks/graphql-qs-projection-model-11]]

    • Format Task 11 runtime changes [[tasks/graphql-qs-projection-model-11]]

    • Bound modeled replay metadata lifetime and decoding [[tasks/graphql-qs-projection-model-11]]

    • Format integrated projection runtime [[tasks/graphql-qs-projection-model-11]]

    • feat: adapt modeled direct projections to exact proof [[tasks/graphql-qs-projection-model-17]]

    • Bind modeled direct proof to active program [[tasks/graphql-qs-projection-model-17]]

    • feat(projections): implement atomic snapshot adapters [[tasks/graphql-qs-projection-model-12]]

    • Harden projection conformance proofs [[tasks/graphql-qs-projection-model-12]]

    • feat: migrate Todo and Chat projection leaves [[tasks/graphql-qs-projection-model-18]]

    • Keep purge descriptor test-only [[tasks/graphql-qs-projection-model-18]]

    • Fix FK-authoritative relationship delegation [[tasks/graphql-qs-projection-model-12]]

    • fix: keep purged todos terminal [[tasks/graphql-qs-projection-model-18]]

    • Test hydrated purge terminality [[tasks/graphql-qs-projection-model-18]]

    • Reject mixed direct projection epochs [[tasks/graphql-qs-projection-model-17]]

    • Harden direct projection owner compatibility [[tasks/graphql-qs-projection-model-17]]

    • feat: apply authoritative projection deltas in JS [[tasks/graphql-qs-projection-model-16]]

    • fix: harden JS projection reconciliation [[tasks/graphql-qs-projection-model-16]]

    • fix: close projection reconciliation audit gaps [[tasks/graphql-qs-projection-model-16]]

    • fix: close projection runtime follow-up gaps [[tasks/graphql-qs-projection-model-16]]

    • feat: model Blob direct projection [[tasks/graphql-qs-projection-model-19]]

    • Implement modeled projection developer experience [[tasks/graphql-qs-projection-model-20]]

    • Fix modeled topology integration [[tasks/graphql-qs-projection-model-20]]

    • Harden modeled projection rollout integration [[tasks/graphql-qs-projection-model-20]]

    • Fence draining projection replay with lifecycle proofs [[tasks/graphql-qs-projection-model-20]]

    • Revalidate terminal projection receipt replays [[tasks/graphql-qs-projection-model-20]]

    • Fix projection draining revalidation bridge [[tasks/graphql-qs-projection-model-20]]

    • Bind empty projection receipts to command contracts [[tasks/graphql-qs-projection-model-20]]

    • Classify mixed draining projection fallback [[tasks/graphql-qs-projection-model-20]]

    • Correct deletion projection examples [[tasks/graphql-qs-projection-model-20]]

    • Fix generated projection UI integration [[tasks/graphql-qs-projection-model-20]]

    • Migrate typed command manifest coverage [[tasks/graphql-qs-projection-model-20]]

    • Strengthen projection manifest coverage [[tasks/graphql-qs-projection-model-20]]

    • Accept embedded model invalidation [[tasks/graphql-qs-projection-model-20]]

    • Constrain embedded invalidation authority [[tasks/graphql-qs-projection-model-20]]

    • Fix PR 127 CI regressions [[incidents/pr-127-ci-failures-after-projection-model]]

    • Wait for durable Todo commands in live test [[incidents/pr-127-ci-failures-after-projection-model]]

    • Fix application projection visibility authority [[incidents/pr-127-ci-failures-after-projection-model]]

    • Infer projected responses from read models [[tasks/graphql-qs-projection-model-21]]

    • Restore explicit CQRS fixture boundaries [[tasks/graphql-qs-projection-model-22]]

    • chore: better version in e2e-ui of test

    • feat: add event-independent mutation IR and dual-path projectors

    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]]

    • feat: placement-selected projected() and wire mutation programs on service 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]]

    • feat: drive e2e mounts from mutation IR rewrite (real resolve path)

    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]]

    • feat: WP10 cutover — remove projection! and command-side .project

    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]]

    • feat: AC4 residual cutover — remove effects macros and public projector 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]]

    • docs: teach placement-selected Blob projected path only

    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]]

    • refactor: release-ready cleanup of leftover projector authoring surfaces

    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]]

    • feat: mutation_projector! sugar and arm helpers for nice app mounts

    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]]

    • docs(e2e-ui): show mutation_projector! author surface in README

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

    • feat: portable_handlers! matches the spec author model

    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]]

    • fix: import DomainEventContract in todo projection tests

    • refactor: remove dual-path compatibility surface; ship portable_handlers 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]]

    • refactor(e2e): rename TODO_READS to TODOS for model-consistent naming

    Matches BLOB_GAMES and CHAT_MESSAGES — the mount is the Todos model
    handlers, not a "reads" collection.

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

    • refactor(e2e-ui): consistent command previews and portable_handlers demos

    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]]

    • feat: event-first mutations, applies mapping, handler-owned projected 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 apply )
    • 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]]

    • fix: keep chat live+optimism with author joins and portable todo lists

    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]]

    • fix: keep warm same-scope cache and polish lobby chat history

    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).

    • fix: restore quality and e2e-ui offline CI on mutation-projectors branch

    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.

    • fix(e2e-ui): grant admin principals the user role too

    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.

    • fix(e2e-ui): write create_human status to stderr for clean UIDs

    Command-substitution of create_human was capturing "reusing human …"
    lines into E2E_HUMAN_*_UID. Keep only the user id on stdout.

    • fix: multi-role application surfaces + chat history load race

    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]]

    • fix(e2e-ui): update surface structural gate and chat optimism window

    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.

    • feat: set-only identity and surface-privilege GraphQL execution

    Breaking major-release cutover: Session carries x-roles only (no
    priority-picked primary x-role). GraphQL execute/stream binds to the opened
    application surface privilege pack (or a membership-checked role surface).
    Multi-role principals without a named surface fail closed. Anonymous
    eligible surfaces open with empty identity.

    e2e-ui: public e2e-ui-public surface, dual-role admin suite uses admin
    surface for elevated ops, causal grants check any asserted role.

    Implements [[tasks/graphql-qs-surface-authz-1]]

    • fix: prove e2e-ui-public anonymous open and finish AuthZ docs

    Add service test that opens e2e-ui-public with an empty Session and queries
    chat_messages, plus unauthenticated /public route documenting the bare
    protocol path. Specs no longer teach Session::role()/x-role as execution.

    Implements [[tasks/graphql-qs-surface-authz-1]]

    • fix: CI failures for set-only identity (x-roles)
    • graphql_query_protocol: use role binding for x-roles header; WS
      connection_init sends x-roles (not x-role)
    • graphql_oidc_common E1: assert Session::roles() instead of role(),
      which is None under set-only claim mapping

    Implements [[tasks/graphql-qs-surface-authz-1]]

    • fix: remove x-role identity bridges — set-only cutover

    Major-release identity is x-roles only; drop migration paths that still
    accepted or re-injected a singleton primary role.

    • ROLE_KEY is x-roles; Session::roles/has_role are the identity API
    • remove causal ensure_causal_grant legacy x-role fallback
    • schema/metrics privilege fallback uses roles set only
    • OIDC e2e layer re-injects x-roles (not x-role + default user)
    • JS DevHeaders + tests send x-roles
    • keep stripping client x-role as defense-in-depth only

    Implements [[tasks/graphql-qs-surface-authz-1]]

    • fix: align subscription unknown-role test with set-only authority

    Unconfigured singleton roles fail closed at execution-authority resolve
    (same generic surface message), not via legacy primary-role schema lookup.

    Implements [[tasks/graphql-qs-surface-authz-1]]

    • fix(e2e-ui): treat scrape outbox duplicates as skips

    Re-scrape of unchanged profiles hits the content-addressed outbox unique
    key by design. Classify DuplicateOutboxMessageInBatch / unique-violation
    wording as skipped, not errors, so start scrape reports stay clean.

    Implements [[tasks/graphql-qs-surface-authz-1]]

    • feat(e2e-ui): framework home + How it’s built slide-out

    Reframe the home page around Distributed principles and link demos as
    destinations. Each demo route gets a right-hand drawer with tabbed
    walkthroughs (domain → command → projection → client) and a principle
    callout per tab — a Distributed lens on the “code tabs next to the app”
    teaching pattern.

    Implements teaching UX for the e2e-ui template.

    • fix(e2e-ui): hero highlights full-stack CQRS, TS, OIDC, SvelteKit

    Lead the home hero with the end-to-end story: event-sourced CQRS, TypeScript
    clients, first-class OIDC, and SvelteKit SSR/live — not CQRS alone.

    • fix(e2e-ui): wider How-it’s-built panel, no closed shadow, code colors

    Drawer only shadows when open, width ~46rem, and lightweight syntax tint
    for walkthrough samples (keywords, strings, types, attrs, comments).

    • feat(e2e-ui): browser-first How-it’s-built tab order

    Reorder every demo walkthrough: (1) query/live (2) commands + client
    cache optimism vs Projected atomic (3) handlers/repo (4) domain macros
    (5) domain events + projections. Match the teaching path from UI inward.

    • feat(e2e-ui): open lobby chat for anonymous GraphQL reads

    Allow /chat without a session so the anonymous privilege pack is visible
    in the UI: e2e-ui-public client for guests, sign-in CTA instead of the
    composer, require_auth=false for empty OIDC identity, and AuthUsers read
    for public author joins.

    • feat(e2e-ui): show RBAC on How-it’s-built query and command tabs

    Each demo walkthrough now includes short ModelPermissions / command.roles
    samples on tabs 1 and 2 so read grants and mutation roles sit next to the
    browser query and command story.

    • fix(e2e-ui): replace comment-only How-it’s-built samples with real code

    Walkthrough panels now paste actual handler, domain, projection, RBAC, and
    generated-client snippets from the fixture instead of comment stubs.

    • feat(e2e-ui): rewrite home as Distributed product landing

    Frame the site as the framework homepage — full-stack CQRS pitch, pillars,
    domain→service→client flow, compact playground cards, and local run.
    Demos stay destinations; ops/hosting only noted as roadmap.

    • style(e2e-ui): product-home layout styles for dist-* sections

    • fix(e2e-ui): reframe home as GraphQL realtime → owned write model

    Drop Meteor comparisons. Pitch Distributed as the next step after live
    GraphQL/query engines: keep the realtime client feel, add event-sourced
    commands, projections, OIDC surfaces, and generated TS clients.

    • fix(e2e-ui): home is CQRS/domain-first; GraphQL is transport

    Position Distributed around event-sourced commands, projections, and an
    honest client replica. GraphQL and SvelteKit are how the playground speaks,
    not the product definition.

    • fix(e2e-ui): brand Distributed; hero product definition

    Rename header/footer e2e-ui → distributed. Hero states Distributed as a
    cloud-native Rust and TypeScript framework for simple realtime, performant,
    scalable apps on distributed-systems foundations.

    • fix(e2e-ui): hero — start simple, scale to microservices

    • feat(e2e-ui): product home Features from CQRS/ES canon narrative

    Rebuild home Features as the owner product story (two models, event-sourced
    aggregates, SQL+RBAC, inferred query edge, projections, browser replica,
    SvelteKit @load/@LiVe, OIDC) with playground code samples and syntax
    highlighting matching How-it's-built panels.

    • refactor: rename portable_handlers! to projection!

    Public event→mutation authoring is now projection! (and compile_projection /
    ProjectionHandler). Matches CQRS product language and ProjectionDescriptor.

    Call sites use distributed::projection! so the macro does not clash with the
    projection module path. Legacy absence tests now assert the declarative macro
    is public while the old event-owning proc-macro stays gone.

    • feat: event-first projection! on { events, mutation, input }

    Replace apply/as and on_deleted with multi-arm on blocks that bind event
    body or aggregate_id into mutation inputs. Align todos/chat/blob, home
    Features (mutation IR + lifetime highlighting), and demos with the new
    surface. Drop mutation_projector and on_state authoring.

    • refactor(e2e-ui): name replica handles query, data by resource

    Prefer query = Op.use() over list, and todos/games/pageMessages over
    generic rows, in app pages, home samples, and How-it's-built demos.

    • feat(e2e-ui): SOTA home story — claim arc, general bar, delivery

    Expand the product home with a Brunson-style arc: claim band, backend/Rust/frontend
    SOTA as general industry bar (not product hooks), handoff to Distributed, then
    backstory and how it delivers. Teach unidirectional + event-driven as one path;
    tighten CAP wording; claim/band styles on home.css.

    • feat(e2e-ui): walkthrough read models + unidirectional flow diagram

    Expand How-it's-built overlays with ReadModel structs, aggregate shapes,
    domain event samples, and projection GraphQL mutations. Replace the home
    system-flow monospace list with a full-width circular dotted diagram.

    • fix(e2e-ui): gate todos/blob on login; drop redundant /public page

    Show Todos and Blob in nav for guests; requireAuth on page loads so
    client-side navigation redirects to /login?callbackUrl=… and returns after
    sign-in. Remove the standalone /public demo (lobby chat already covers
    anonymous). Honor callbackUrl on login/signup when already signed in;
    hero copy mentions realtime applications.

    • feat: Eventual/Atomic command semantics + restore chat optimism

    Ship one mutation IR with two proofs: Eventual (async projector +
    delta/expects) and Atomic (handler row + records). Rename wire/protocol
    states and APIs from causal/projected to eventual/atomic with no aliases.

    Direct placements export .applies previews for client optimism while
    still sealing from the atomic response. Command ledger migrations use
    atomic state (0004 + CHECK updates).

    Fix chat list optimism regressions: belongs_to joins are GraphQL/client
    nullable so missing author edges materialize, and full first-page offset
    indexes accept local optimistic inserts (re-sort + truncate).

    Regenerate e2e-ui clients, demos/docs, and JS tests for the new contract.

    • test(e2e-ui): gate demo optimism offline and in the browser

    Add a shared hold-mutation helper and optimism.user.spec that requires
    chat post (including full first page), todos create/complete, and blob
    move continuity to paint before a delayed GraphQL response.

    Offline optimism-artifacts.test.mjs locks preview IR, atomic
    directProjection, nullable ChatMessages.author, and local first-page
    insert policy so gen/compiler regressions fail without a browser.

    Todos create under the delayed-route order test now asserts list paint
    before the wire returns.

    • fix(e2e-ui): blob move board optimism via .applies input fields

    Wire blob.move like todos/chat: command input carries the optimistic
    board outcome (map_json, score, status, …) and state_preview maps those
    fields into the client optimistic layer. A pure TypeScript twin of
    blob_domain::simulate_move fills the input; the handler still recomputes
    authority from game_id + direction only.

    Regenerate clients (full upsert preview), unit-test simulate_move parity,
    and require paint-before-wire on the player cell in optimism.user.spec.

    • docs(e2e-ui): clarify blob move uses shared .applies optimism path

    • refactor!: drop TypedCommand.preview alias — use applies only

    No back-compat renames. Call sites and docs use .applies. Wire
    vocabulary test expects eventual/atomic only (rejects causal/projected).

    • docs: application composition — logical mounts, process roles, runtime

    Capture the accepted DX direction: same packages re-cut as monolith or
    microservices; Eventual projectors may split; Atomic seals stay collocated.
    Runtime pairs persistence, locks, and bus; process role selects outbox,
    consumer, and GraphQL. Implementation order and e2e-ui collapse targets
    included. Linked from usage skill and e2e-ui README.

    • fix(ci): finish eventual/atomic rename in CLI fixtures and suite asserts

    Update generated-commands fixture, dctl client_compiler/cli_manifest
    expectations, GraphQL protocol tests, and e2e-ui behavioral suite to the
    eventual/atomic wire vocabulary (no causal/projected aliases).

    • fix: repair PR 170 migration and contract drift

    Preserve applied migration history, register migration 4, complete the Eventual/Atomic rename, refresh generated contracts, and repair the affected e2e behavior.

    Resolves [[pr-170-eventual-atomic-rename-and-migration-failur]]

    • test(e2e-ui): consolidate duplicate coverage

    Fold optimistic assertions into product journeys, remove redundant offline and browser scenarios, and retain the unique stale-response race.\n\nImplements [[tasks/e2e-ui-test-maintenance-3]].\nImplements [[tasks/e2e-ui-test-maintenance-4]].

    • test(js): centralize command protocol fixtures

    Route valid command metadata through one canonical fixture while keeping malformed protocol cases explicit.\n\nImplements [[tasks/e2e-ui-test-maintenance-5]].

    • ci: remove duplicate feedback work

    Cancel superseded PR runs, deduplicate e2e-ui setup, narrow compatibility gates, and compile Postgres tests with only required features.\n\nImplements [[tasks/e2e-ui-test-maintenance-6]].\nImplements [[tasks/e2e-ui-test-maintenance-7]].

    • ci: overlap independent e2e-ui offline gates

    Install shared UI prerequisites once, then run the Rust and sequential UI pipelines concurrently.\n\nImplements [[tasks/e2e-ui-test-maintenance-6]].

  • feat: land application lifecycle and workbench stack (by @patrickleet)

    BREAKING CHANGE: Squash the complete stacked change set through PR #178 after GitHub's
    stack merge operation landed only the bottom layer.

    Includes PRs #172, #173, #174, #177, and #178.

    BREAKING CHANGE: the standalone CLI binary is named distributed instead
    of dctl, and application surface roles use the eligible/schema split.

See full diff: v3.3.4...v4.0.0