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:
- The user sees exactly what will happen before it happens.
- The commit cannot diverge from the preview — both derive from the same canonicalized payload, so the UUID and the result are identical.
- 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