Prototype: RecipeDraft(name,ingredients,steps) retains ordered list copies and snapshot returns {name,ingredients,steps} with fresh lists. Empty/duplicate items and empty name allowed; no permissions.
Final: RecipeManager.addUser(userId,permissions) requires nonempty ID, only read/write strings (duplicates harmless), validates even duplicate IDs, returns false preserving existing permissions. add(userId,name,ingredients,steps) requires write and nonempty current unique name; returns recipe1,recipe2,... on success, null on denial/collision; failures consume no ID. update appends even identical content and returns version, null for denied/missing/name conflict; renaming frees old current name. get(userId,id,version=null) requires read; null latest, nonexistent integer revision returns null. history returns detached revisions in order or []. rollback(userId,id,version) requires write, appends selected content as new version unless missing/name conflict, returning version/null. byIngredient requires read, matches current exact membership once per recipe, sorts ingredient-list length/name/ID. Snapshot keys exactly {id,version,name,ingredients,steps}. Noninteger revision arguments raise RangeError; empty names raise before permission checks.
Every class is exported by name from exercise.mjs. Keep instances independent. Unless explicitly documented otherwise, input values have the stated collection/string types and do not contain null elements. Invalid calls preserve state; validate before mutation. Return fresh mutable snapshots unless stated otherwise.
Complete the starter code, then run the checks.
Results and errors will appear here.