# Placeholder System Guidelines

This note is the working guideline for Lumi placeholder and variable support.
Future placeholder work should follow this model unless the architecture is
explicitly revised.

## Architecture

Placeholders are registered server-side through `src/services/placeholders.js`.
The frontend must not declare placeholder permissions, allowed plugins, or
sensitivity. Editable fields reference a trusted `field_id`, and the server
uses that field policy to decide which placeholders are available.

Platform membership and role duration placeholders, including their embedded
`{format.YY-MM-DD}` grammar, are documented in
[platform-tenure.md](platform-tenure.md).

Core and plugins can register:

- placeholder definitions with metadata and a resolver function
- field policies for template/input destinations that support placeholders

Catalog, preview, save validation, and runtime rendering must share the same
permission-checking path. Saved templates are not trusted permanently; they must
be revalidated at render time because plugin availability, policies, settings,
or sensitivity can change.

## Naming

Use namespaced double-brace tokens:

- `{{core.main.bot_name}}`
- `{{core.main.command_prefix}}`
- `{{core.command.rng}}`
- `{{custom.stream.title}}`
- `{{user.public.display_name}}`
- `{{platform.discord.guild.member_count}}`
- `{{okf.file.community.currency.primary_name}}`
- `{{plugin.throne_wishlist.item_name}}`

Avoid unqualified names for new work. Compatibility aliases may exist for old
templates, but the catalog should present canonical namespaced tokens.

Admin-managed values live under `{{custom.*}}`. Admins create them under
Settings > Reusable text values; names are validated, cannot collide with
system/plugin namespaces, and are stored in the update-preserved settings
database.

## Core Namespaces

Current core-owned namespaces include:

- `core.main.*`: safe Lumi core settings such as bot/site name and command prefix
- `core.command.*`: values created for one command execution, such as a Random Reply RNG roll
- `custom.*`: admin-managed reusable values with explicit user, moderator, or admin visibility
- `user.public.*`: safe viewer/triggering-user display information
- `platform.discord.guild.*`: safe Discord guild statistics from the configured guild
- `platform.twitch.channel.*`: safe locally configured Twitch channel/runtime values
- `platform.youtube.channel.*`: safe locally configured or hydrated YouTube channel/runtime values
- `okf.file.*`: file-backed OKF frontmatter values, with legacy aliases such as `{{community.currency.primary_name}}`

Each platform namespace exposes a public-safe `data_status` value. Current values are `live_cache`, `connected`, `configured_not_connected`, or `unavailable`, depending on the integration. Discord statistics read only the connected client's guild cache; placeholder rendering never triggers a Discord fetch.

Twitch follower/subscriber counts and Twitch/YouTube scheduled-start values remain intentionally unavailable. The current Twitch integration is chat-only, and the YouTube runtime does not maintain those statistics. Add them only after a background refresh service stores value, fetched-at time, expiry/stale state, and last error. Rendering must read that cache without a provider API call and return a safe unavailable/stale result when credentials or rate limits prevent refresh.

## Sensitivity

Supported sensitivity levels:

- `public_safe`: safe in user-visible output
- `user`: user-specific content, only for fields whose output audience allows it
- `moderator`: moderator/support visible content
- `admin`: admin-only content
- `internal`: internal implementation/runtime content
- `secret_never_render`: must not be registered, listed, previewed, or rendered

Never expose API keys, tokens, cookies, passwords, database URLs, session IDs,
raw secrets, private file paths, sensitive query strings, or unredacted raw
diagnostics through placeholders. If diagnostics are needed, register explicit
redacted safe variants.

## Field Policies

Every placeholder-compatible input must use a stable `field_id` whose policy is
registered by trusted backend code. A field policy should define:

- `field_id`
- label and field type
- output audience
- minimum editor role
- allowed namespaces or placeholder IDs
- maximum sensitivity

Security is based on the intersection of:

- current editor role/capability during catalog, preview, and save
- field policy
- output audience that will see rendered content
- placeholder sensitivity
- placeholder minimum viewer role
- plugin availability
- runtime context

Current trusted field policies:

| Field ID | Destination | Editor | Output | Allowed values |
| --- | --- | --- | --- | --- |
| `core.custom_commands.static_response` | Static and Random Reply custom-command replies | Moderator | User | Public-safe `core.main.*`, `core.command.*`, `custom.*`, and `user.public.*` |
| `plugin.throne_wishlist.message_template` | Throne event announcements | Admin | User | Public-safe Throne event placeholders |
| `okf.markdown` | SQLite/community OKF Markdown | OKF contributor | Selected role | Permission-filtered core, platform, and visible OKF values |

The current WebUI contains no inline JSON placeholder catalogs; double-brace editors use these server-owned IDs. Welcome Messages and Auto VC retain their older `{username}` and `[username]` runtime token formats for compatibility. They are not treated as generic placeholders until their event-context renderers and saved templates are migrated together; adding only autocomplete would advertise tokens those runtimes cannot safely resolve.

An admin editing a user-visible template is not enough to allow admin-only
placeholders. The output audience still limits what can render.

Custom values use the same catalog, validation, preview, and runtime checks as
code-registered placeholders. Values marked for moderators or admins do not
appear in lower-privilege catalogs and cannot render into user-visible command
replies. The `custom` namespace is reserved for this registry; plugins must use
their own `plugin.*` namespace.

## Plugin Registration

Plugins should register placeholders during backend plugin initialization using
the placeholder service passed to `init` or available through
`web.placeholders`. Plugin definitions should include `plugin_id` so disabled or
missing plugins can fail safely.

Resolvers must return safe display values and avoid leaking raw internal data.
Renderers that send chat messages or HTML should still sanitize or escape for
their output context after placeholder resolution.

## Frontend

Frontend fields use:

```html
data-placeholder-field="plugin.example.message_template"
data-placeholder-output-audience="user"
```

The frontend requests `/api/placeholders/catalog?field_id=...` and uses the
returned catalog for autocomplete and tree browsing. Inline JSON placeholder
lists are a transitional fallback only and should not be used for new fields.

## Validation

Templates must be checked:

- when the editor opens a catalog
- during preview
- before save
- at runtime before rendering output

Unauthorized placeholders should fail closed. User-visible output should show a
generic unavailable marker or leave legacy unknown tokens unchanged, depending
on the existing renderer contract, but it must not reveal the restricted value.

Async resolvers are bounded by a short timeout (1.5 seconds by default). Duplicate and adjacent tokens are resolved by source position, and resolver output is inserted literally rather than scanned again. A failed, unavailable, forbidden, or timed-out value therefore affects only that token and renders the caller's safe fallback.
