@hyprune/schema 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -390,3 +390,76 @@ Publishes IPC 0.21 (markers, personal quests, focus sessions) and IPC 0.22 (obje
390
390
  ## @hyprune/schema 0.8.0 (2026-10-12)
391
391
 
392
392
  Publishes IPC 0.23 and world format 0.9: characters and NPC instances, authored paths (route/track/generic) with stops, seats, keepClear volumes and NPC interactions.
393
+
394
+ ## Unreleased: object manifest 0.10 draft additions
395
+
396
+ Draft only; no version changes. `v0.10/object.schema.json` and the validator:
397
+
398
+ - Mappings take an `average` modifier `{windowS? (1–3600, default 60),
399
+ override?}`: the running average of a numeric value, alone or under `scale`,
400
+ `thresholds`, `cases` or `format`; never with `becomes` or `counter`. The
401
+ validator requires an `int` or `number` input, and in drives an `override`
402
+ that names another non-event input of the machine (recipes take no override).
403
+ A direct average of an `int` is fractional, so counts need a rounded scale.
404
+ - New slot kind `levels` (at most 8): `{id, node, mode: fill|slide, axis?
405
+ (unit vector, default [0, 1, 0]), length (> 0, ≤ 10), surface? (fill only),
406
+ settle?: ease|spring}` and drive target `level:<id>` (outputs 0–1). With the
407
+ glTF: `node` and `surface` exist in the scene, the surface is not inside the
408
+ fill node's subtree, no node is moved by two slots, and the collision proxy
409
+ is never moved.
410
+ - `stacks`: `cap` up to 100 (was 64) and an optional `rate` (copies per second,
411
+ 0–1000). `layout.spacing` components may be negative (−10 to 10 m; copies fill
412
+ from the node towards the sign) and must be non-zero where that axis's grid is
413
+ above 1.
414
+
415
+ ## IPC 0.24: machine inputs in the catalog and live machine state (RFC-0013)
416
+
417
+ - `objects.catalog` entries gain required `machine`: null for an entry without
418
+ the machine trait, else `{primary: input id|null, inputs[1..16]}`. Each input
419
+ is the object 0.10 declaration as authored: `{id, type (enum|int|number|bool|
420
+ text|event|items), label, explain, default?, values? (enum), min?/max?
421
+ (int, number), maxLength? (text), repeat? (2–16, not expanded), states?
422
+ (items)}`.
423
+ - Required state key `live {instances[<=64]}`, published on change and never
424
+ per frame. An instance is `{id, asset, ref|null, source|null (the source
425
+ every bound input reads, null when they differ), status (ok|error|null, the
426
+ source's _status), error (<=256|null, its _error), stale, inputs}`; `inputs`
427
+ maps up to 16 input ids, repeated inputs expanded (`health.1`), to `{source|null,
428
+ key|null, value, raw, condition (fresh|stale|missing|error), publishedAt}`.
429
+ Values are scalars (strings <=256) or null; `publishedAt` is the wall-clock
430
+ Unix ms of the publication core holds, null when missing.
431
+ - Everything else is inherited from 0.23 unchanged (no new method, grant or
432
+ event); `session.hello` reports `0.24`. Inheritance test included.
433
+
434
+ ## @hyprune/schema 0.9.0 (2026-10-13)
435
+
436
+ Publishes IPC 0.24: `objects.catalog` entries carry a placed machine's declared inputs (`machine`), and a new `live` state key reports each placed machine's source, status and per-input values (published only on change). Inherited from 0.23 otherwise; no new grant. Also the object 0.10 draft additions for gauges: the `average` mapping modifier, `levels` slots (fill/slide with spring) and stack rate, cap 100 and signed spacing.
437
+
438
+ ## IPC 0.25 (draft): companions (RFC-0010 companions amendment)
439
+
440
+ - Methods (grant `objects.control`): `companion.set {ref}` (a companion-capable
441
+ library or user catalog ref), `companion.clear {}` and `companion.mode {mode:
442
+ follow|stay}`; each returns `{revision}`. Existing methods take the reserved
443
+ object id `companion`: `npcs.bind` (a route or a standing spot), `paths.draw
444
+ {bind}` and `interaction.choose`. No new grant, no new event.
445
+ - Required state key `companion {ref|null, name|null (<=32), state
446
+ (none|loading|unavailable|active|waiting|hidden), reason|null (<=64), mode
447
+ (follow|stay|route), path|null (qualified path ref), activity|null
448
+ (following|walking|running|idle|talking|sitting|waiting|carried),
449
+ position|null, carried}`.
450
+ - `objects.catalog` entries gain required `companion`: null, or `{capable,
451
+ name, reason|null}`.
452
+ - `npcs.aimed.interactions[].type` gains `companion` (Stay and Follow).
453
+ - Everything else is inherited from 0.24 unchanged; `session.hello` reports
454
+ `0.25`. Inheritance test included.
455
+
456
+ ## Object format 0.10 (draft): companion block
457
+
458
+ - Optional `companion {name? (1-32), followDistance? (1.5-2.5 m), idles?
459
+ (<=4 unique clip names), say? (<=8 lines of <=160 characters)}`, only on
460
+ characters; idle variants must be clips of `traits.character` and not
461
+ canonical RFC-0010 clip names (`talk` is a new canonical name).
462
+
463
+ ## @hyprune/schema 0.10.0 (2026-10-13)
464
+
465
+ Publishes IPC 0.25: companions. `companion.set`, `companion.clear` and `companion.mode` (follow or stay), a required `companion` state key, a companion capability on catalog entries and the aimed interaction type `companion`; and the object 0.10 draft `companion` block (name, followDistance, idles, say). Inherits 0.24.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Hyprune** is an explorable, world-driven desktop environment for Hyprland: your workspaces become places you can walk through, with live windows on the walls. This repository holds the versioned **schemas** for world packages, IPC and manifests, and their validator. Install Hyprune through the [hyprune](https://github.com/hyprune/hyprune) repository (starter kits, Nix, Arch); learn more at [hyprune.com](https://hyprune.com). Licence: MIT.
4
4
 
5
- Versioned contracts for Hyprune, a world-driven Hyprland desktop: world packages (formats 0.1–0.9), the IPC protocol (0.1–0.23), object and library manifests (0.9, draft 0.10), extension and screen-type manifests and the input keymap. JSON Schema dialect: **2020-12**. Every version is frozen once published; core, the shell and the SDK pin these exact files. Readable reference: [hyprune.com/reference/schema](https://hyprune.com/reference/schema/).
5
+ Versioned contracts for Hyprune, a world-driven Hyprland desktop: world packages (formats 0.1–0.9), the IPC protocol (0.1–0.25, 0.25 draft), object and library manifests (0.9, draft 0.10), extension and screen-type manifests and the input keymap. JSON Schema dialect: **2020-12**. Every version is frozen once published; core, the shell and the SDK pin these exact files. Readable reference: [hyprune.com/reference/schema](https://hyprune.com/reference/schema/).
6
6
 
7
7
  ## Install
8
8
 
@@ -203,6 +203,22 @@ values, `use`/`name`/`speed`/`stops` on editor paths, the reticle action
203
203
  (objects.control), `paths.list` (state.read) and `interaction.choose`
204
204
  (world.control). Every other 0.22 definition is unchanged.
205
205
 
206
+ IPC **0.24** adds live machine state (RFC-0013): `objects.catalog` entries gain
207
+ a required `machine` (null, or the machine trait's `primary` and its input
208
+ declarations as authored, repeat not expanded) and a required `live` state key
209
+ lists at most 64 bound machine instances with each input's binding, mapped and
210
+ raw value, condition (`fresh`, `stale`, `missing`, `error`) and `publishedAt`
211
+ (wall-clock Unix ms of the held publication, or null). No new method, event or
212
+ grant; every other 0.23 definition is unchanged. See the RFC-0013 amendment
213
+ "Live state in IPC 0.24".
214
+
215
+ IPC **0.25** (draft, the RFC-0010 companions amendment) adds the companion: a
216
+ required `companion` state key (the chosen ref and name, its state, mode
217
+ `follow`/`stay`/`route`, bound path, activity and position), a `companion`
218
+ capability on catalog entries, the aimed interaction type `companion`, and
219
+ `companion.set/clear/mode` (objects.control). Object format 0.10 (draft) gains
220
+ the optional `companion` block. Every other 0.24 definition is unchanged.
221
+
206
222
  ## Objects and libraries (RFC-0011)
207
223
 
208
224
  Object manifests (`object.json`) and library manifests (`library.json`) use the
package/lib/validate.mjs CHANGED
@@ -257,6 +257,17 @@ function validateObjectGltf(value, version, gltf, options, fail, warn) {
257
257
  }
258
258
  }
259
259
  for (const s of slots.stacks || []) node(s.node, `slots.stacks ${s.id}`);
260
+ const levelNodes = new Map();
261
+ for (const l of slots.levels || []) {
262
+ const where = `slots.levels ${l.id}`;
263
+ for (const name of [l.node, l.surface]) {
264
+ if (name === undefined) continue;
265
+ if (name === traits.collidable?.node) { fail(`${where}: the collision proxy can't be moved`); continue; }
266
+ if (levelNodes.has(name)) fail(`${where}: node "${name}" is also moved by levels slot ${levelNodes.get(name)}`); else levelNodes.set(name, l.id);
267
+ }
268
+ const i = node(l.node, where), j = l.surface === undefined ? undefined : node(l.surface, where);
269
+ if (i !== undefined && j !== undefined && subtree(i).includes(j)) fail(`${where}: surface "${l.surface}" must not be inside the fill node's subtree`);
270
+ }
260
271
  for (const t of slots.toggles || []) for (const name of t.nodes) {
261
272
  if (name === traits.collidable?.node) fail(`slots.toggles ${t.id}: the collision proxy can't be toggled`);
262
273
  node(name, `slots.toggles ${t.id}`);
@@ -328,7 +339,18 @@ function validateMachine(value, fail) {
328
339
  return input;
329
340
  };
330
341
  // What a mapping can produce from an input: a set of scalars, a numeric range, or a kind.
342
+ const numeric = input => input?.type === 'int' || input?.type === 'number';
331
343
  const outputs = (map, input) => {
344
+ if (map?.average) {
345
+ // The form applies to the running average (or the override's value): an int input's
346
+ // average is fractional, and an override widens a direct range.
347
+ const {average, ...form} = map, o = outputs(form, input);
348
+ if (Object.keys(form).length) return o;
349
+ const over = average.override === undefined ? undefined : inputs.get(average.override.split('.')[0]);
350
+ if (o.range && numeric(over)) o.range = [Math.min(o.range[0], over.min), Math.max(o.range[1], over.max)];
351
+ else if (over && !numeric(over)) return { kind: over.type };
352
+ return o.range ? { ...o, integer: false } : o;
353
+ }
332
354
  if (!map || !Object.keys(map).length) {
333
355
  if (input.type === 'enum') return { values: input.values };
334
356
  if (input.type === 'bool') return { values: [true, false] };
@@ -355,16 +377,25 @@ function validateMachine(value, fail) {
355
377
  const [kind, name] = [drive.target.slice(0, drive.target.indexOf(':')), drive.target.slice(drive.target.indexOf(':') + 1)];
356
378
  const known = { animation: () => animated.has(name), position: () => positions.has(name), state: () => palettes.has(name), emission: () => palettes.has(name),
357
379
  label: () => slotIds('labels').has(name), visible: () => slotIds('toggles').has(name), count: () => slotIds('stacks').has(name),
358
- spawn: () => slotIds('spawns').has(name), effect: () => slotIds('effects').has(name) }[kind];
380
+ spawn: () => slotIds('spawns').has(name), effect: () => slotIds('effects').has(name), level: () => slotIds('levels').has(name) }[kind];
359
381
  if (!known()) { fail(`${where}: unknown ${kind} target "${name}"`); continue; }
360
382
  if (!input) continue;
383
+ if (drive.map?.average) {
384
+ const ref = drive.map.average.override;
385
+ if (!numeric(input)) { fail(`${where}: average needs an int or number input`); continue; }
386
+ if (ref !== undefined) {
387
+ const over = resolve(ref, `${where} average override`);
388
+ if (ref === drive.input) fail(`${where}: average override must name another input`);
389
+ else if (over && (over.type === 'event' || over.type === 'items')) fail(`${where}: average override ${ref} must be a value input, not ${over.type}`);
390
+ }
391
+ }
361
392
  const o = outputs(drive.map, input), event = o.kind === 'event' || (input.type === 'event' && !drive.map);
362
393
  if (kind === 'state') {
363
394
  if (!o.values) fail(`${where}: needs palette ids (an enum input or cases)`);
364
395
  else for (const v of o.values) if (!palettes.get(name).has(v)) fail(`${where}: "${v}" is not a palette id of ${name}`);
365
396
  } else if (kind === 'animation') within(o, 0, 10, where);
366
397
  else if (kind === 'emission') within(o, 0, 16, where);
367
- else if (kind === 'position') within(o, 0, 1, where);
398
+ else if (kind === 'position' || kind === 'level') within(o, 0, 1, where);
368
399
  else if (kind === 'count') { within(o, 0, Infinity, where); if (o.range && !o.integer) fail(`${where}: counts need an int input or a rounded scale`); }
369
400
  else if (kind === 'visible') { if (!o.values || o.values.some(v => typeof v !== 'boolean')) fail(`${where}: needs boolean outputs`); }
370
401
  else if (kind === 'spawn' || kind === 'effect') { if (!event) fail(`${where}: needs an event (an event input, counter or becomes)`); }
@@ -378,6 +409,8 @@ function validateMachine(value, fail) {
378
409
  for (const ref of Object.keys(recipe.fields)) resolve(ref, `${label} recipe ${recipe.name}`);
379
410
  for (const [ref, map] of Object.entries(recipe.maps || {})) {
380
411
  const input = resolve(ref, `${label} recipe ${recipe.name}`);
412
+ if (map.average && input && !numeric(input)) fail(`${label} recipe ${recipe.name}: ${ref} average needs an int or number input`);
413
+ if (map.average?.override !== undefined) fail(`${label} recipe ${recipe.name}: ${ref} average override is for drives only`);
381
414
  if (input?.type === 'enum') for (const v of outputs(map, { type: 'text' }).values || [])
382
415
  if (!input.values.includes(v)) fail(`${label} recipe ${recipe.name}: ${ref} maps to "${v}", not a value of the input`);
383
416
  }
@@ -390,6 +423,8 @@ function validateMachine(value, fail) {
390
423
  function traitCombinations(names, fail) {
391
424
  for (const a of names) for (const c of names) if (a < c && !(traitPairs[a] || []).includes(c)) fail(`traits: ${a} and ${c} can't be combined`);
392
425
  }
426
+ // RFC-0010 canonical character clip names (talk: the companions amendment).
427
+ const canonicalClips = ['idle', 'idle_fidget', 'walk', 'run', 'turn_left', 'turn_right', 'look_around', 'wave', 'nod', 'sit', 'sit_idle', 'stand', 'talk'];
393
428
  function objectTraitRules(traits, slots, fail, inside = () => {}) {
394
429
  const quat = (q, where) => { if (Math.abs(Math.hypot(...q) - 1) > 0.002) fail(`${where}: quaternion must be normalized`); };
395
430
  const unique = (items, field, label) => { const ids = items.map(v => v[field]); if (new Set(ids).size !== ids.length) fail(`${label}: duplicate ${field}`); };
@@ -430,15 +465,24 @@ function objectTraitRules(traits, slots, fail, inside = () => {}) {
430
465
  if (!tr) fail(`slots.spawns ${s.id}: unknown track ${s.track}`);
431
466
  else for (const a of s.items || []) if (tr.items && !tr.items.assets.includes(a)) fail(`slots.spawns ${s.id}: item ${a} is not in track ${tr.id}'s items`);
432
467
  }
468
+ for (const l of slots.levels || []) if (l.axis && Math.abs(Math.hypot(...l.axis) - 1) > 0.001) fail(`slots.levels ${l.id}: axis must be a unit vector`);
433
469
  for (const s of slots.stacks || []) {
434
470
  if (s.cap > s.layout.grid.reduce((x, y) => x * y, 1)) fail(`slots.stacks ${s.id}: cap exceeds the layout grid`);
435
- s.layout.grid.forEach((n, j) => { if (n > 1 && !(s.layout.spacing[j] > 0)) fail(`slots.stacks ${s.id}: spacing ${'xyz'[j]} must be positive for ${n} cells`); });
471
+ s.layout.grid.forEach((n, j) => { if (n > 1 && !(Number.isFinite(s.layout.spacing[j]) && s.layout.spacing[j] !== 0)) fail(`slots.stacks ${s.id}: spacing ${'xyz'[j]} must be non-zero for ${n} cells`); });
436
472
  }
437
473
  }
438
474
  function validateObjectManifest(value, version, options, fail, warn) {
439
475
  const traits = value.traits, slots = value.slots || {}, names = Object.keys(traits), b = value.bounds;
440
476
  if (options.directory !== undefined && options.directory !== value.id) fail(`id: "${value.id}" differs from its directory "${options.directory}"`);
441
477
  traitCombinations(names, fail);
478
+ // Companion block (object format 0.10 draft, RFC-0010 companions amendment).
479
+ if (value.companion) {
480
+ if (!traits.character) fail('companion: only characters can be companions');
481
+ else for (const clip of value.companion.idles || []) {
482
+ if (!(clip in traits.character.clips)) fail(`companion: idle variant "${clip}" is not a clip of traits.character`);
483
+ if (canonicalClips.includes(clip)) fail(`companion: "${clip}" is a canonical clip, not an idle variant`);
484
+ }
485
+ }
442
486
  for (let j = 0; j < 3; j++) if (b.min[j] >= b.max[j]) fail(`bounds: min ${'xyz'[j]} must be below max`);
443
487
  const t = objectLimits.boundsTolerance, surfaces = new Set(traits.movable ? traits.movable.surfaces || ['floor'] : []);
444
488
  const one = surfaces.size === 1 ? [...surfaces][0] : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyprune/schema",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Versioned JSON Schemas for Hyprune world packages and the IPC protocol, with a validator and CLI for world authors",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -420,7 +420,7 @@
420
420
  "target": {
421
421
  "type": "string",
422
422
  "maxLength": 160,
423
- "pattern": "^(animation|state|emission|label|visible|count|spawn|effect|position):[^:\\s]{1,128}$"
423
+ "pattern": "^(animation|state|emission|label|visible|count|spawn|effect|position|level):[^:\\s]{1,128}$"
424
424
  },
425
425
  "map": {
426
426
  "$ref": "#/$defs/mapping"
@@ -647,6 +647,45 @@
647
647
  "additionalProperties": false,
648
648
  "description": "Trait name to trait data. Unknown or reserved names reject the object."
649
649
  },
650
+ "companion": {
651
+ "type": "object",
652
+ "properties": {
653
+ "name": {
654
+ "type": "string",
655
+ "minLength": 1,
656
+ "maxLength": 32,
657
+ "pattern": "^[^\\u0000-\\u001f\\u007f]+$"
658
+ },
659
+ "followDistance": {
660
+ "type": "number",
661
+ "minimum": 1.5,
662
+ "maximum": 2.5
663
+ },
664
+ "idles": {
665
+ "type": "array",
666
+ "items": {
667
+ "type": "string",
668
+ "minLength": 1,
669
+ "maxLength": 64,
670
+ "pattern": "^[a-z][a-z0-9_]*$"
671
+ },
672
+ "maxItems": 4,
673
+ "uniqueItems": true
674
+ },
675
+ "say": {
676
+ "type": "array",
677
+ "items": {
678
+ "type": "string",
679
+ "minLength": 1,
680
+ "maxLength": 160,
681
+ "pattern": "^[^\\u0000-\\u001f\\u007f]+$"
682
+ },
683
+ "maxItems": 8
684
+ }
685
+ },
686
+ "additionalProperties": false,
687
+ "description": "Companion settings (RFC-0010 companions amendment, draft): display name, follow distance, idle variants (clips of traits.character) and Talk lines. Characters only."
688
+ },
650
689
  "slots": {
651
690
  "type": "object",
652
691
  "properties": {
@@ -1005,7 +1044,7 @@
1005
1044
  "cap": {
1006
1045
  "type": "integer",
1007
1046
  "minimum": 1,
1008
- "maximum": 64
1047
+ "maximum": 100
1009
1048
  },
1010
1049
  "layout": {
1011
1050
  "type": "object",
@@ -1025,10 +1064,11 @@
1025
1064
  "items": {
1026
1065
  "type": "number",
1027
1066
  "maximum": 10,
1028
- "minimum": 0
1067
+ "minimum": -10
1029
1068
  },
1030
1069
  "minItems": 3,
1031
- "maxItems": 3
1070
+ "maxItems": 3,
1071
+ "description": "Metres between copies per axis; a negative component fills from the node towards -axis. Non-zero where that axis's grid is above 1."
1032
1072
  },
1033
1073
  "fill": {
1034
1074
  "enum": [
@@ -1047,6 +1087,12 @@
1047
1087
  "fill"
1048
1088
  ],
1049
1089
  "additionalProperties": false
1090
+ },
1091
+ "rate": {
1092
+ "type": "number",
1093
+ "minimum": 0,
1094
+ "maximum": 1000,
1095
+ "description": "Copies per second the shown count moves toward the target; 0 or absent: at once."
1050
1096
  }
1051
1097
  },
1052
1098
  "required": [
@@ -1089,6 +1135,78 @@
1089
1135
  "minItems": 1,
1090
1136
  "maxItems": 8
1091
1137
  },
1138
+ "levels": {
1139
+ "type": "array",
1140
+ "items": {
1141
+ "type": "object",
1142
+ "properties": {
1143
+ "id": {
1144
+ "$ref": "../v0.8/common.schema.json#/$defs/slug"
1145
+ },
1146
+ "node": {
1147
+ "type": "string",
1148
+ "minLength": 1,
1149
+ "maxLength": 128
1150
+ },
1151
+ "mode": {
1152
+ "enum": [
1153
+ "fill",
1154
+ "slide"
1155
+ ]
1156
+ },
1157
+ "axis": {
1158
+ "type": "array",
1159
+ "items": {
1160
+ "type": "number",
1161
+ "minimum": -1,
1162
+ "maximum": 1
1163
+ },
1164
+ "minItems": 3,
1165
+ "maxItems": 3
1166
+ },
1167
+ "length": {
1168
+ "type": "number",
1169
+ "exclusiveMinimum": 0,
1170
+ "maximum": 10
1171
+ },
1172
+ "surface": {
1173
+ "type": "string",
1174
+ "minLength": 1,
1175
+ "maxLength": 128
1176
+ },
1177
+ "settle": {
1178
+ "enum": [
1179
+ "ease",
1180
+ "spring"
1181
+ ]
1182
+ }
1183
+ },
1184
+ "required": [
1185
+ "id",
1186
+ "node",
1187
+ "mode",
1188
+ "length"
1189
+ ],
1190
+ "additionalProperties": false,
1191
+ "not": {
1192
+ "properties": {
1193
+ "mode": {
1194
+ "const": "slide"
1195
+ },
1196
+ "surface": {
1197
+ "type": "string"
1198
+ }
1199
+ },
1200
+ "required": [
1201
+ "mode",
1202
+ "surface"
1203
+ ]
1204
+ },
1205
+ "description": "A subtree scaled (fill) or moved (slide) by level:<id>, 0-1. axis: asset-space unit vector, default [0, 1, 0]; surface: fill only; settle default ease."
1206
+ },
1207
+ "minItems": 1,
1208
+ "maxItems": 8
1209
+ },
1092
1210
  "ports": {
1093
1211
  "type": "array",
1094
1212
  "items": {
@@ -1548,10 +1666,58 @@
1548
1666
  }
1549
1667
  },
1550
1668
  "additionalProperties": false
1669
+ },
1670
+ "average": {
1671
+ "type": "object",
1672
+ "properties": {
1673
+ "windowS": {
1674
+ "type": "number",
1675
+ "minimum": 1,
1676
+ "maximum": 3600
1677
+ },
1678
+ "override": {
1679
+ "type": "string",
1680
+ "minLength": 1,
1681
+ "maxLength": 40,
1682
+ "pattern": "^[a-z][A-Za-z0-9]*(\\.[1-9][0-9]?)?$"
1683
+ }
1684
+ },
1685
+ "additionalProperties": false,
1686
+ "description": "Running average modifier: the time-weighted mean of the value over the last windowS seconds (default 60) of fresh data; the mapping's form applies to it. override (drives only): another non-event input whose value, while present, replaces the average."
1551
1687
  }
1552
1688
  },
1553
1689
  "additionalProperties": false,
1554
- "description": "RFC-0013 mapping language: direct ({}), cases, thresholds, scale, becomes, counter or format."
1690
+ "description": "RFC-0013 mapping language: direct ({}), cases, thresholds, scale, becomes, counter or format; average modifies the value forms (not becomes or counter).",
1691
+ "not": {
1692
+ "anyOf": [
1693
+ {
1694
+ "properties": {
1695
+ "average": {
1696
+ "type": "object"
1697
+ },
1698
+ "becomes": {}
1699
+ },
1700
+ "required": [
1701
+ "average",
1702
+ "becomes"
1703
+ ]
1704
+ },
1705
+ {
1706
+ "properties": {
1707
+ "average": {
1708
+ "type": "object"
1709
+ },
1710
+ "counter": {
1711
+ "const": true
1712
+ }
1713
+ },
1714
+ "required": [
1715
+ "average",
1716
+ "counter"
1717
+ ]
1718
+ }
1719
+ ]
1720
+ }
1555
1721
  },
1556
1722
  "input": {
1557
1723
  "type": "object",