@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 +44 -0
- package/README.md +10 -1
- package/lib/validate.mjs +37 -3
- package/package.json +1 -1
- package/v0.10/object.schema.json +132 -5
- package/v0.24/ipc.schema.json +19679 -0
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.
|
|
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]
|
|
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
package/v0.10/object.schema.json
CHANGED
|
@@ -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":
|
|
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":
|
|
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",
|