Skip to content

Mutation Pipeline

Verified against the code · 4996d1ceb770fb4545edebc5fd140792bfb23830 · 2026-10-20

956 D1b-4 (2026-10-20): 1 finding(s) fixed — mutations_router.py route pins are now node/validate :204, node/preview :224, node/commit :270, node-update/preview :324, node-update/commit :355, relation/preview :414, relation/commit :452 (paths and status codes unchanged). Re-verified against main 4996d1ce.

flowchart LR
%% generated_from: mutation-pipeline
%% verified_at_commit: 4996d1ceb770fb4545edebc5fd140792bfb23830
%% 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 The router exposes three literal per-operation preview routes — there is no parametrised {entity} segment:

  • POST /mutations/node/validate (fail-fast required-field check — the only stage below without a Commit twin; backend/api/mutations_router.py:204)
  • POST /mutations/node/preview (mutations_router.py:224)
  • POST /mutations/node-update/preview (mutations_router.py:324)
  • POST /mutations/relation/preview (mutations_router.py:414)

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 Three literal per-operation commit routes mirror the preview set:

  • POST /mutations/node/commit (status 201; mutations_router.py:270)
  • POST /mutations/node-update/commit (status 200; mutations_router.py:355)
  • POST /mutations/relation/commit (status 201; mutations_router.py:452)

The handler takes { previewId }, the mutation-store calls write_atomic (write to 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-crud — Zone: frontend

form validation before Preview button activates

Invariants enforced: preview-before-commit

Preview Generation

Engine: mutation-engine — Zone: api

POST /mutations/node/preview, /mutations/node-update/preview, or /mutations/relation/preview (literal per-operation routes; no {entity} segment) — validate payload, derive UUID v5, return diff without writing

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

Commit

Engine: mutation-store — Zone: domain

POST /mutations/node/commit, /mutations/node-update/commit, or /mutations/relation/commit (literal per-operation routes; no {entity} segment) — write overlay file atomically (fsync + rename)

Invariants enforced: atomic-write, deterministic-uuid

Cache Invalidation

Engine: eidos-loader — Zone: domain

invalidate EffectiveGraph _cache so next read triggers a fresh merge_overlay pass

Invariants enforced: overlay-wins-on-conflict