feat(events): a param that holds several values (MODULE_API 1.14.0)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 47s
PR Checks / client-build (pull_request) Successful in 48s
PR Checks / server-tests (pull_request) Successful in 13m55s

Rust PLAN_REDESIGNS §12.3 (D344): an event action param may declare
`multiple: true`, with an optional `max` (1-100, 20 when left out), on a
`string` or `int` param. The permission reward steps take several permissions
in one step through it.

- registries: the flag and `max` are checked at registration and served in the
  catalog; refused on other types, on a duration, on a template's inputs, and
  with a list as the example. A param that does not say it reads back exactly
  as declared, without the keys.
- spec: the value is a JSON array, each item the type's literal, duplicates
  dropped in order, at most `max`; a bare value is a list of one; `required`
  means at least one; an empty item is refused.
- authorize: a multiple param's example is priced as a list of one, since
  cost() is handed the array.
- editor: a multi-select. Chips above a grouped, filterable checklist for a
  source that answered; the typeahead for a searchable source; a tag box
  otherwise. Ticking past max is refused in words. A new step holds the
  example as a list of one.
- run console: params read as `name: value`, a list joined by commas.
- MODULE_API 1.13.0 -> 1.14.0 on both halves; engagement-triggers.json and the
  catalog's OpenAPI note regenerated.

Additive only: Module-uo (`^1.10.0`) declares no multiple param.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-12 15:40:30 -05:00
parent ca7ec53b14
commit 743f1bc8b2
14 changed files with 616 additions and 14 deletions

View File

@@ -144,7 +144,9 @@ function priceOf(action, params) {
function dimensionsOf(action) {
const params = {}
for (const p of action?.params || []) {
if (p.example !== undefined && p.example !== null) params[p.name] = p.example
// A `multiple` param's example is one value and `cost()` is handed a list
// (1.14.0), so it is priced as a list of one.
if (p.example !== undefined && p.example !== null) params[p.name] = p.multiple ? [p.example] : p.example
}
const priced = priceOf(action, params)
return priced ? Object.keys(priced).sort() : []

View File

@@ -323,6 +323,37 @@ function validateAdvance(raw, path, errors) {
return { on: triggerId, where: checked.conditions, count, dormant: false }
}
/**
* A `multiple` param's value as the list it is (MODULE_API 1.14.0, Rust
* PLAN_REDESIGNS §12.3).
*
* `{ value: [...] }` or `{ error }`. **A bare value is a list of one**, so a
* spec written by hand or over the API before its author knew the param took
* several does not trip on it. Absent, `null`, `''` and `[]` are all the empty
* list, which the caller reads as unset — `required` means at least one.
*
* Each element is the type's literal, checked by the same `checkLiteral` a
* single value is; an empty string inside the list is refused rather than kept,
* because a blank chip is nothing anyone picked. Duplicates are dropped keeping
* the first, so the order the author picked in is the order the module reads.
*/
function checkList(p, raw) {
if (raw === undefined || raw === null || raw === '') return { value: [] }
const items = Array.isArray(raw) ? raw : [raw]
const out = []
const seen = new Set()
for (let i = 0; i < items.length; i++) {
if (items[i] === '') return { error: `item ${i + 1} is empty` }
const checked = checkLiteral(p.type, items[i])
if (checked.error) return { error: `item ${i + 1} ${checked.error}` }
if (seen.has(checked.value)) continue
seen.add(checked.value)
out.push(checked.value)
}
if (out.length > p.max) return { error: `holds ${out.length} values; the most is ${p.max}` }
return { value: out }
}
/**
* Check one authored param object against an action's declared params.
*
@@ -349,6 +380,14 @@ function checkParams(declaration, raw, path) {
for (const p of declaration.params || []) {
const value = given[p.name]
if (p.multiple) {
const listed = checkList(p, value)
if (listed.error) errors.push(`${path}.params: "${p.name}" ${listed.error}`)
else if (!listed.value.length) {
if (p.required) errors.push(`${path}.params: "${p.name}" is required`)
} else params[p.name] = listed.value
continue
}
if (value === undefined || value === null || value === '') {
if (p.required) errors.push(`${path}.params: "${p.name}" is required`)
continue