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

@@ -1030,11 +1030,23 @@ const MAX_BUDGET_MS = 3_600_000
// vocabulary of one is a vocabulary that can grow without a version.
const PARAM_DURATIONS = ['minutes']
// A param that holds several values (MODULE_API 1.14.0; Rust PLAN_REDESIGNS
// §12.3, D344). Two types, because the one step that needed it picks strings
// from a source and an int list is the same check one type over; a list of
// booleans, dates or links is a shape nothing has asked for, and every control
// the editor draws for a list is a picker or a tag box.
const MULTIPLE_PARAM_TYPES = ['string', 'int']
// `max` when the declaration leaves it out, and the most it may say. The ceiling
// is a bound on one step's params, not a number anyone should reach: a list of a
// hundred is a step nobody can read back.
const DEFAULT_MULTIPLE_MAX = 20
const MAX_MULTIPLE_MAX = 100
// `call` names the registration in the error, because a template's inputs are
// checked by this same function (they ARE params: one control each, with a
// name, a type, an example and maybe a source).
function checkActionParam(actionId, entry, seen, call = 'registerEventActions') {
const { name, type, required, example, description, source, duration } = entry || {}
const { name, type, required, example, description, source, duration, multiple, max } = entry || {}
const where = `${call}: ${actionId}`
if (!PARAM_NAME.test(name || '')) throw new Error(`${where}: bad param name "${name}"`)
if (seen.has(name)) throw new Error(`${where}: param "${name}" declared twice`)
@@ -1072,6 +1084,35 @@ function checkActionParam(actionId, entry, seen, call = 'registerEventActions')
throw new Error(`${where}: param "${name}" is a duration and must be int or float, not ${type}`)
}
}
// **`multiple` makes the value a list** (1.14.0, §12.3). The example stays ONE
// value — it is the placeholder of the box a value is typed into — and the
// value a step stores is a JSON array of them (`events/spec.js`).
//
// Refused on a duration, because a list of lengths of time is not a hold the
// editor can keep a phase open for, and on a template's inputs, because
// `build()` is a module's own code reading `inputs.x` as one value and this
// version does not change what it is handed.
if (multiple !== undefined && multiple !== null && multiple !== false) {
if (multiple !== true) throw new Error(`${where}: param "${name}" multiple must be true or left out`)
if (call === 'registerEventTemplates') {
throw new Error(`${where}: input "${name}" cannot be multiple; a template's inputs are single values`)
}
if (!MULTIPLE_PARAM_TYPES.includes(type)) {
throw new Error(`${where}: param "${name}" is multiple and must be ${MULTIPLE_PARAM_TYPES.join(' or ')}, not ${type}`)
}
if (duration !== undefined && duration !== null) {
throw new Error(`${where}: param "${name}" cannot be both multiple and a duration`)
}
if (Array.isArray(example)) {
throw new Error(`${where}: param "${name}" needs ONE example, not a list (it is the placeholder of one value)`)
}
}
if (max !== undefined && max !== null) {
if (multiple !== true) throw new Error(`${where}: param "${name}" has a max but is not multiple`)
if (!Number.isInteger(max) || max < 1 || max > MAX_MULTIPLE_MAX) {
throw new Error(`${where}: param "${name}" max must be 1..${MAX_MULTIPLE_MAX}, got "${max}"`)
}
}
return {
name,
type,
@@ -1080,6 +1121,9 @@ function checkActionParam(actionId, entry, seen, call = 'registerEventActions')
source: source === undefined ? null : source,
description: description || '',
...(duration ? { duration } : {}),
// Absent rather than `false` on every other param, as `duration` is, so a
// module that never says it reads back exactly the declaration it wrote.
...(multiple === true ? { multiple: true, max: max ?? DEFAULT_MULTIPLE_MAX } : {}),
}
}
@@ -2164,6 +2208,9 @@ module.exports = {
allEventTemplates,
eventTemplate,
PARAM_DURATIONS,
MULTIPLE_PARAM_TYPES,
DEFAULT_MULTIPLE_MAX,
MAX_MULTIPLE_MAX,
resolveOptionSource,
allEngagementSeeds,
engagementSeedsFor,