Skip to content

Mutation Pipeline

Verified — minor divergences from the code — 1 finding(s) · 19d408be · 2026-08-25

The core claims check out against the actual implementation: preview issues a server-side previewId via mutation_store.write_preview and commit consumes it single-use via consume_preview (returning 400 PREVIEW_REQUIRED on reuse); derive_entity_uuid (UUID v5) is called inside mutation_engine.build_preview during the preview pass, guaranteeing preview-UUID == commit-UUID; mutation_store.write_atomic does temp-file write → fsync → os.replace; cache invalidation bumps a monotonic sentinel counter that eidos_loader picks up to re-run domain.graph_merge.merge_overlay. The one divergence is the doc's generic route notation, which doesn't reflect the real, non-parametrized endpoint set.

Divergences from the code — details
Sev Where Doc says Code does Evidence
MEDIUM Stage-by-Stage — "2. Preview Generation" and "3. Commit" "POST /mutations/{entity}/preview" and "POST /mutations/{entity}/commit { previewId }" are presented as a single parametrized route pattern covering all entity mutations. There is no {entity} path parameter in the actual API. The routes are separate literal paths per operation: POST /mutations/node/preview, POST /mutations/node/commit, POST /mutations/node-update/preview, POST /mutations/node-update/commit, POST /mutations/relation/preview, POST /mutations/relation/commit (backend/api/mutations_router.py:12-18, handlers at lines 175, 217, 261, 290, 349, 378). "node-update" in particular doesn't fit an "entity" substitution model — it's an operation name, not an entity type. backend/api/mutations_router.py:12-18 (route list in module docstring), :175 and :217 (actual @router.post decorators)
flowchart LR
%% generated_from: mutation-pipeline
%% verified_at_commit: d50caf3143612a76bde48787104e90a4ac202ff3
%% description: Mutation Pipeline lifecycle
%% legend: aw=atomic-write, du=deterministic-uuid, owoc=overlay-wins-on-conflict, pbc=preview-before-commit
  subgraph frontend
    frontend-validation["Frontend Validation · pbc"]
  end
  subgraph api
    preview-generation["Preview Generation · du pbc"]
  end
  subgraph domain
    commit["Commit · aw du"]
    cache-invalidation["Cache Invalidation · owoc"]
  end
  frontend-validation --> preview-generation
  preview-generation --> commit
  commit --> cache-invalidation

Every write operation in EIDOS — creating a node, updating a node, adding a relation — passes through the same pipeline. Understanding this pipeline is essential for both backend contributors and anyone debugging mutation failures.

The Core Design: Preview Before Commit

EIDOS never writes a mutation directly. Every mutation first goes through a preview pass that validates the payload and returns a diff — without writing anything. Only after the user confirms the preview does the commit pass execute.

This design gives three guarantees:

  1. The user sees exactly what will happen before it happens.
  2. The commit cannot diverge from the preview — both derive from the same canonicalized payload, so the UUID and the result are identical.
  3. A previewId is single-use — committing twice with the same previewId returns 400, making accidental double-writes impossible.

Invariant: deterministic-uuid

The UUID assigned to a new entity is derived by derive_entity_uuid — a UUID v5 hash of the canonicalized command payload. This means:

  • Preview UUID == Commit UUID (same input → same UUID — CCR Rule 16)
  • Import re-runs are idempotent (re-importing the same file produces the same IDs)
  • The UUID is not a secret — it is a deterministic function of content

This is why random UUID4 is forbidden for entity IDs. Random UUIDs break idempotency: a preview and its commit would generate different IDs, and re-importing the same data would silently create duplicates.

Stage-by-Stage

1. Frontend Validation The form validates locally before the Preview button activates. Required fields must be present; known constraint violations (e.g. empty label) are caught here. This is a UX guard — the backend validates independently.

2. Preview Generation POST /mutations/{entity}/preview — the mutation-engine validates the payload against the EffectiveGraph (parent exists, path not already taken, IAM access permitted), derives the UUID v5, and returns a preview diff. No file is written. If validation fails, has_errors=true and previewId is null — the frontend's Confirm button must stay disabled (I-2).

3. Commit POST /mutations/{entity}/commit { previewId } — the mutation-store calls write_atomic: write to a temp file, fsync, then os.replace to the target path. The fsync ensures the data survives a power loss between the write and the rename. After the rename, the overlay file is visible atomically — readers either see the old version or the new version, never a partial write.

4. Cache Invalidation After a successful commit, the mutation-store bumps the monotonic cache-version sentinel. The eidos-loader detects the sentinel bump on the next request and rebuilds the EffectiveGraph from scratch by re-running merge_overlay over the updated overlay directory. This ensures the new mutation is immediately visible.

What Happens on Failure

  • Preview validation fails: has_errors=true, previewId=null — frontend shows error, Confirm stays disabled. No state change.
  • Commit fails (network/server error): The overlay file was not written (the rename is atomic). The EffectiveGraph is unchanged. The user can retry.
  • Commit succeeds but cache is stale: The sentinel bump is synchronous with the write. The next read will trigger a cache rebuild. There is no window where a committed mutation is invisible.

Stage Reference

Frontend Validation

Engine: frontend-crudZone: frontend

form validation before Preview button activates

Invariants enforced: preview-before-commit

Preview Generation

Engine: mutation-engineZone: api

POST /mutations/{entity}/preview — validate payload, derive UUID v5, return diff without writing

Invariants enforced: deterministic-uuid, preview-before-commit

Commit

Engine: mutation-storeZone: domain

POST /mutations/{entity}/commit — write overlay file atomically (fsync + rename)

Invariants enforced: atomic-write, deterministic-uuid

Cache Invalidation

Engine: eidos-loaderZone: domain

invalidate EffectiveGraph _cache so next read triggers a fresh merge_overlay pass

Invariants enforced: overlay-wins-on-conflict