Profiles & activation
A profile is a root-level capability selector: it decides which plugins — and therefore which vocabulary — are active for a scene. The project declares a profile graph and a defaultProfile in lute.project.yaml; a scene picks one with frontmatter profile: and MAY layer scene-local plugins: on top.
The profile graph
Section titled “The profile graph”profiles: global: # reserved name; inherited by every profile plugins: { lute.core: true } lint: { unknownDirective: error } story: plugins: { arcia.vn: true } date: extends: story plugins: { arcia.date: { phoneSurface: enabled } } date-minigame: extends: date plugins: arcia.minigame: { resultScope: scene, allowedKinds: [rhythm, timing] }
defaultProfile: storyglobalis reserved and is applied before any other profile — every profile inherits it, which is wherelute.core’s directives, attributes, and vocabulary slots come from. Since0.9.0it is only the slots:lute.coreships an emptyenums:, so the members are the project’s to declare.extendsnames a single parent profile; the chain MUST be acyclic. Parents apply before children.pluginsis a map from plugin id to a typed option object, ortrue(which normalizes to defaults). Presence of a legal key activates that plugin. There is noplugins.uselist, and there is no scene-local deactivation — afalsevalue is a static error (E-PROFILE-PLUGIN-VALUE), never an off switch. To exclude a plugin, do not inherit a profile that activates it.
Selecting a profile in a scene
Section titled “Selecting a profile in a scene”A scene names its profile in frontmatter (absent ⇒ defaultProfile) and MAY add scene-local plugins: — additive only. Here the scene narrows allowedKinds so rhythm is the only legal minigame kind:
---kind: scenecharacter: marinaseason: 1episode: 5pov: fixerprofile: date-minigameplugins: arcia.minigame: resultScope: scene allowedKinds: [rhythm]---(Frontmatter of docs/examples/arcia-project/date-minigame.lute, whose lute.project.yaml declares the profile graph above.)
Resolution & merge
Section titled “Resolution & merge”Activation resolves deterministically in this exact order: lute.core → global → the selected profile’s extends chain (parent first) → the selected profile → scene-local plugins: → the dependency closure. When the same plugin’s options are set at multiple layers, later layers win: scalar values override, map values deep-merge, and list values replace by default. The result is exactly one option object per active plugin and exactly one capability snapshot.
A reference to a directive, attribute, or id from an installed-but-inactive plugin is a diagnostic with fix-its (“change profile” / “activate plugin”) — never silently accepted syntax.
Activation is not permission
Section titled “Activation is not permission”A profile can also carry a
permissions: ceiling over its resolved
surface. These are different axes:
plugins:selects which capabilities exist and how their typed options merge;permissions:can only deny use of directives, writes, bridges, rewards, or quests that already exist.
Project-root, global, parent, and selected-profile permission layers are
conjunctive. Missing fields are unrestricted; an explicit empty list denies the
whole category. Scene-local plugins: remains additive but cannot widen a
permission deny. Permission policy is never accepted from scene frontmatter or
plugin exports.
Because source-authored profile: is not authorization, a host accepting
untrusted authored input adds --permission-profile NAME to check, compile,
compile —stream, or context. That trusted pin contributes only the named
profile’s permission ceiling—it neither activates its plugins nor changes the
profile written in source.
Options are checked, not just merged
Section titled “Options are checked, not just merged”Activation validates every supplied option against the owning plugin’s options: declaration. The check runs on the post-merge value, so only the final layered value is judged — a bad value in a parent profile that a child overrides is never reported, and a bad final value is reported once no matter how many layers set it.
An option name the plugin does not declare is E-PLUGIN-OPTION-UNKNOWN, and the message names the whole declared set so the fix is usually a typo away:
$ lute check scene.lute --project .lute: E-PLUGIN-OPTION-UNKNOWN: plugin `arcia.bonus` has no option `resultScop` (declared: resultScope, rounds)A value that fails its declared type is E-PLUGIN-OPTION-TYPE. Every violation in the profile is collected in one pass rather than bailing on the first:
$ lute check scene.lute --project .lute: E-PLUGIN-OPTION-TYPE: option `arcia.bonus.resultScope` expects enum(scene|run), got "galaxy"lute: E-PLUGIN-OPTION-TYPE: option `arcia.bonus.rounds` expects int, got "three"Both describe the project rather than a span in any one file, and both are errors: they print without a file position and they set the CLI exit code. They travel the same resolution channel as load errors and unresolved depends, so the CLI, the checker, and the LSP observe them identically.
An option no layer sets is absent from the merge and takes its declared default, which is not re-checked — the declaration is the source of both the type and the value.