ADR-0008 Blueprint provisioning for roles, scales and cohorts¶
- Status: Accepted
- Date: 2026-06-08
Context and Problem¶
The blueprint system could provision users, courses, categories, sections, enrolments, modules, plugins and language packs, but had no way to define roles, grading scales or cohorts. These are essential for realistic Moodle setups: custom roles (e.g. a course "coordinator", or a guest that can see hidden courses), competency/grading scales, and site cohorts.
Real-world inputs already exist in two shapes:
- Roles are routinely authored in the Moodle admin UI and exported as role preset XML
(Site administration → Users → Permissions → Define roles → Export) — a
<role>document with archetype, context levels, hundreds of capabilities, andallowassign/allowoverride/allowswitch/allowviewrelationships. - Scales are commonly captured as JSON (a
{ "format": "moodle-scale-export", "scales": [...] }envelope where each scale has anameand a comma-separateditemsstring).
Two requirements drove the design: data must be definable inline in the blueprint and by reference to an external JSON/XML (URL or similar); and the result should generalize so the remaining "and the rest" data (cohorts now; custom fields, competencies later) is cheap to add.
Options Considered¶
-
Option 1 — A new "external file" mechanism per step. Add bespoke
rolesUrl/scalesUrlfields and fetch logic in each handler. Rejected: it duplicates the existing resource system and invents a parallel, inconsistent way to reference external data. -
Option 2 — JSON-only roles. Express every role as a JSON capability map. Rejected as the sole mechanism: real exports carry hundreds of capabilities and relationship tables; re-encoding them as JSON by hand is impractical, and we would be reimplementing Moodle's own preset parser.
-
Option 3 — Reuse the resource system; add domain steps; for roles support both the native XML preset (via Moodle core's parser) and a JSON-native form. New steps consume the existing
ResourceRegistry(url/base64/data-url/bundled/vfs/literal+@name), exactly likewriteFile/unzip, so "load from a URL" needs no new code.
Decision¶
Option 3. Add three step domains, each definable inline or by @resource/URL through the
existing resource system:
- Roles — two complementary steps.
importRolePreset/importRolesimport native Moodle role preset XML, parsed by Moodle core itself (core_role_preset::parse_preset/is_valid_preset). A two-pass apply (create/update all role records, then context levels + system-context capabilities + relationship tables) lets interdependent roles reference each other. Capabilities of uninstalled plugins are skipped. Idempotent onshortname. The generated PHP is adapted from a referenceapply-role-presets.phpbut accepts XML strings (no filesystem in WASM CLI mode).-
createRole/createRolesdefine roles from a JSON map (capabilities,contextlevels,allow*relationships, optionalresetToArchetype). Permission and context-level names are resolved to integers in JS so only safe values reach PHP. -
Scales —
createScale/createScales.itemsaccepts an array or a comma-separated string.createScalesalso accepts themoodle-scale-exportenvelope so an exported file works as-is. Idempotent on(courseid, name)via thegrade_scaleclass; omittingcoursecreates a site-wide standard scale. -
Cohorts —
createCohort/createCohorts. Site-context cohorts viacohort_add_cohort, with optionalmembers(usernames →cohort_add_member). Idempotent onidnumber(orname).
A shared src/blueprint/steps/payload.js resolves a batch step's payload from an inline array, a
@name reference, an inline resource descriptor, or a raw JSON/XML string. All generated PHP runs
in CLI_SCRIPT mode, escapes input via escapePhp(), installs a graceful set_exception_handler
(JSON {"ok":false} + exit(0) instead of killing WASM — see ADR-0005), and echoes JSON.
Consequences¶
Positive¶
- One way to reference external data. Inline and URL/
@resourcecome from the same resource system already used elsewhere; no new fetch path, no new CORS/proxy surface. - High-fidelity roles for free. Real exported roles drop straight in because Moodle's own parser applies them; the JSON form stays available for small, readable roles.
- Idempotent and non-fatal, consistent with the rest of the blueprint executor (ADR-0005).
- Extensible. The same handler+generator+resolver recipe covers cohorts now and future data (custom fields, competencies) with minimal code.
Negative / Risks¶
importRolesdepends onadmin/roles/classes/preset.php. It is core Moodle (ships in the runtime bundle), but a future core refactor ofcore_role_presetwould require revisiting the generator.- Two ways to define roles (XML and JSON) is slightly more surface to document; mitigated by a clear "use XML for exports, JSON for small custom roles" guideline.
- Capability skipping is silent per role (counted, not failed) — a capability for a missing plugin is dropped rather than erroring, matching the reference importer's behaviour.
Implementation Notes¶
Files added¶
src/blueprint/steps/moodle-roles.js,moodle-scales.js,moodle-cohorts.js— step handlers.src/blueprint/steps/payload.js— shared inline/@resource/URL/raw payload resolution.tests/blueprint/moodle-roles.test.js,moodle-scales.test.js,moodle-cohorts.test.js.assets/blueprints/examples/roles-scales-cohorts.blueprint.json— runnable example.
Files modified¶
src/blueprint/php/helpers.js—phpImportRolePresets,phpCreateRoles,phpCreateScales,phpCreateCohorts,normalizeScaleItems, and a shared graceful handler.src/blueprint/steps/index.js— register the three step groups.src/blueprint/schema.js,assets/blueprints/blueprint-schema.json— add the 8 step names (the JSON schema enum also picked up the previously missingsetTheme/installLanguagePack).tests/blueprint/steps.test.js— extend the expected step list.docs/blueprint-json.md,docs/blueprint-gallery.md— document the steps, the inline-vs-URL model, and the extension pattern.
Review Criteria¶
- If Moodle core changes
core_role_preset(parsing API or the role preset XML schema), re-verifyimportRolesagainst a fresh export. - If the
grade_scale,create_role/assign_capability, orcohort_*APIs change, re-verify the affected generator. - If the resource system gains new descriptor types, confirm
payload.js'sisResourceDescriptor()still distinguishes descriptors from inline domain data (e.g. the scale envelope).