@hyprune/schema 0.8.0 → 0.9.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,47 @@ 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.
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.24), 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,15 @@ 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
+
206
215
  ## Objects and libraries (RFC-0011)
207
216
 
208
217
  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
  }
@@ -430,9 +463,10 @@ function objectTraitRules(traits, slots, fail, inside = () => {}) {
430
463
  if (!tr) fail(`slots.spawns ${s.id}: unknown track ${s.track}`);
431
464
  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
465
  }
466
+ 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
467
  for (const s of slots.stacks || []) {
434
468
  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`); });
469
+ 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
470
  }
437
471
  }
438
472
  function validateObjectManifest(value, version, options, fail, warn) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyprune/schema",
3
- "version": "0.8.0",
3
+ "version": "0.9.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"
@@ -1005,7 +1005,7 @@
1005
1005
  "cap": {
1006
1006
  "type": "integer",
1007
1007
  "minimum": 1,
1008
- "maximum": 64
1008
+ "maximum": 100
1009
1009
  },
1010
1010
  "layout": {
1011
1011
  "type": "object",
@@ -1025,10 +1025,11 @@
1025
1025
  "items": {
1026
1026
  "type": "number",
1027
1027
  "maximum": 10,
1028
- "minimum": 0
1028
+ "minimum": -10
1029
1029
  },
1030
1030
  "minItems": 3,
1031
- "maxItems": 3
1031
+ "maxItems": 3,
1032
+ "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
1033
  },
1033
1034
  "fill": {
1034
1035
  "enum": [
@@ -1047,6 +1048,12 @@
1047
1048
  "fill"
1048
1049
  ],
1049
1050
  "additionalProperties": false
1051
+ },
1052
+ "rate": {
1053
+ "type": "number",
1054
+ "minimum": 0,
1055
+ "maximum": 1000,
1056
+ "description": "Copies per second the shown count moves toward the target; 0 or absent: at once."
1050
1057
  }
1051
1058
  },
1052
1059
  "required": [
@@ -1089,6 +1096,78 @@
1089
1096
  "minItems": 1,
1090
1097
  "maxItems": 8
1091
1098
  },
1099
+ "levels": {
1100
+ "type": "array",
1101
+ "items": {
1102
+ "type": "object",
1103
+ "properties": {
1104
+ "id": {
1105
+ "$ref": "../v0.8/common.schema.json#/$defs/slug"
1106
+ },
1107
+ "node": {
1108
+ "type": "string",
1109
+ "minLength": 1,
1110
+ "maxLength": 128
1111
+ },
1112
+ "mode": {
1113
+ "enum": [
1114
+ "fill",
1115
+ "slide"
1116
+ ]
1117
+ },
1118
+ "axis": {
1119
+ "type": "array",
1120
+ "items": {
1121
+ "type": "number",
1122
+ "minimum": -1,
1123
+ "maximum": 1
1124
+ },
1125
+ "minItems": 3,
1126
+ "maxItems": 3
1127
+ },
1128
+ "length": {
1129
+ "type": "number",
1130
+ "exclusiveMinimum": 0,
1131
+ "maximum": 10
1132
+ },
1133
+ "surface": {
1134
+ "type": "string",
1135
+ "minLength": 1,
1136
+ "maxLength": 128
1137
+ },
1138
+ "settle": {
1139
+ "enum": [
1140
+ "ease",
1141
+ "spring"
1142
+ ]
1143
+ }
1144
+ },
1145
+ "required": [
1146
+ "id",
1147
+ "node",
1148
+ "mode",
1149
+ "length"
1150
+ ],
1151
+ "additionalProperties": false,
1152
+ "not": {
1153
+ "properties": {
1154
+ "mode": {
1155
+ "const": "slide"
1156
+ },
1157
+ "surface": {
1158
+ "type": "string"
1159
+ }
1160
+ },
1161
+ "required": [
1162
+ "mode",
1163
+ "surface"
1164
+ ]
1165
+ },
1166
+ "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."
1167
+ },
1168
+ "minItems": 1,
1169
+ "maxItems": 8
1170
+ },
1092
1171
  "ports": {
1093
1172
  "type": "array",
1094
1173
  "items": {
@@ -1548,10 +1627,58 @@
1548
1627
  }
1549
1628
  },
1550
1629
  "additionalProperties": false
1630
+ },
1631
+ "average": {
1632
+ "type": "object",
1633
+ "properties": {
1634
+ "windowS": {
1635
+ "type": "number",
1636
+ "minimum": 1,
1637
+ "maximum": 3600
1638
+ },
1639
+ "override": {
1640
+ "type": "string",
1641
+ "minLength": 1,
1642
+ "maxLength": 40,
1643
+ "pattern": "^[a-z][A-Za-z0-9]*(\\.[1-9][0-9]?)?$"
1644
+ }
1645
+ },
1646
+ "additionalProperties": false,
1647
+ "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
1648
  }
1552
1649
  },
1553
1650
  "additionalProperties": false,
1554
- "description": "RFC-0013 mapping language: direct ({}), cases, thresholds, scale, becomes, counter or format."
1651
+ "description": "RFC-0013 mapping language: direct ({}), cases, thresholds, scale, becomes, counter or format; average modifies the value forms (not becomes or counter).",
1652
+ "not": {
1653
+ "anyOf": [
1654
+ {
1655
+ "properties": {
1656
+ "average": {
1657
+ "type": "object"
1658
+ },
1659
+ "becomes": {}
1660
+ },
1661
+ "required": [
1662
+ "average",
1663
+ "becomes"
1664
+ ]
1665
+ },
1666
+ {
1667
+ "properties": {
1668
+ "average": {
1669
+ "type": "object"
1670
+ },
1671
+ "counter": {
1672
+ "const": true
1673
+ }
1674
+ },
1675
+ "required": [
1676
+ "average",
1677
+ "counter"
1678
+ ]
1679
+ }
1680
+ ]
1681
+ }
1555
1682
  },
1556
1683
  "input": {
1557
1684
  "type": "object",