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:
- 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
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-crud — Zone: frontend
form validation before Preview button activates
Invariants enforced: preview-before-commit
Preview Generation¶
Engine: mutation-engine — Zone: api
POST /mutations/{entity}/preview — validate payload, derive UUID v5, return diff without writing
Invariants enforced: deterministic-uuid, preview-before-commit
Commit¶
Engine: mutation-store — Zone: domain
POST /mutations/{entity}/commit — 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