@hyprune/schema 0.0.0-stage → 0.5.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/LICENSE +21 -0
  3. package/README.md +140 -2
  4. package/bin/validate.mjs +19 -0
  5. package/examples/extension.json +22 -0
  6. package/examples/ipc/error.json +11 -0
  7. package/examples/ipc/movement.set.request.json +8 -0
  8. package/examples/ipc/movement.set.response.json +7 -0
  9. package/examples/ipc/runtime.exit.request.json +6 -0
  10. package/examples/ipc/runtime.exit.response.json +7 -0
  11. package/examples/ipc/runtime.notice.notification.json +9 -0
  12. package/examples/ipc/session.hello.request.json +17 -0
  13. package/examples/ipc/session.hello.response.json +23 -0
  14. package/examples/ipc/session.ping.request.json +6 -0
  15. package/examples/ipc/session.ping.response.json +7 -0
  16. package/examples/ipc/session.revoked.notification.json +7 -0
  17. package/examples/ipc/shell.claim.request.json +10 -0
  18. package/examples/ipc/shell.claim.response.json +8 -0
  19. package/examples/ipc/shell.overlay.request.json +11 -0
  20. package/examples/ipc/shell.overlay.response.json +7 -0
  21. package/examples/ipc/shell.release.request.json +8 -0
  22. package/examples/ipc/shell.release.response.json +7 -0
  23. package/examples/ipc/state.delta.notification.json +18 -0
  24. package/examples/ipc/state.snapshot.request.json +8 -0
  25. package/examples/ipc/state.snapshot.response.json +37 -0
  26. package/examples/ipc/state.subscribe.request.json +10 -0
  27. package/examples/ipc/state.subscribe.response.json +38 -0
  28. package/examples/ipc/state.unsubscribe.request.json +8 -0
  29. package/examples/ipc/state.unsubscribe.response.json +7 -0
  30. package/examples/ipc/surface.focus.request.json +9 -0
  31. package/examples/ipc/surface.focus.response.json +7 -0
  32. package/examples/ipc/world.load.request.json +9 -0
  33. package/examples/ipc/world.load.response.json +7 -0
  34. package/examples/placement-input.json +169 -0
  35. package/examples/screen-type.json +24 -0
  36. package/examples/world.json +111 -0
  37. package/extensions/hud-style-manifest.schema.json +17 -0
  38. package/extensions/hud-style.schema.json +118 -0
  39. package/lib/validate.d.mts +37 -0
  40. package/lib/validate.mjs +221 -0
  41. package/package.json +57 -4
  42. package/v0/common.schema.json +85 -0
  43. package/v0/extension.schema.json +121 -0
  44. package/v0/ipc.schema.json +1725 -0
  45. package/v0/screen-type.schema.json +115 -0
  46. package/v0/world.schema.json +363 -0
  47. package/v0.10/ipc.schema.json +8811 -0
  48. package/v0.11/ipc.schema.json +8863 -0
  49. package/v0.12/ipc.schema.json +9541 -0
  50. package/v0.13/ipc.schema.json +10032 -0
  51. package/v0.14/ipc.schema.json +10127 -0
  52. package/v0.15/ipc.schema.json +10534 -0
  53. package/v0.16/ipc.schema.json +10550 -0
  54. package/v0.17/ipc.schema.json +10703 -0
  55. package/v0.18/ipc.schema.json +10801 -0
  56. package/v0.19/ipc.schema.json +11587 -0
  57. package/v0.2/common.schema.json +86 -0
  58. package/v0.2/ipc.schema.json +3570 -0
  59. package/v0.2/world.schema.json +363 -0
  60. package/v0.3/common.schema.json +477 -0
  61. package/v0.3/ipc.schema.json +4044 -0
  62. package/v0.3/world.schema.json +390 -0
  63. package/v0.4/common.schema.json +477 -0
  64. package/v0.4/ipc.schema.json +5561 -0
  65. package/v0.4/world.schema.json +496 -0
  66. package/v0.5/common.schema.json +477 -0
  67. package/v0.5/ipc.schema.json +5781 -0
  68. package/v0.5/world.schema.json +790 -0
  69. package/v0.6/common.schema.json +478 -0
  70. package/v0.6/input.schema.json +519 -0
  71. package/v0.6/ipc.schema.json +7375 -0
  72. package/v0.6/world.schema.json +1062 -0
  73. package/v0.7/common.schema.json +478 -0
  74. package/v0.7/ipc.schema.json +7905 -0
  75. package/v0.7/world.schema.json +1062 -0
  76. package/v0.8/ipc.schema.json +7945 -0
  77. package/v0.9/input.schema.json +518 -0
  78. package/v0.9/ipc.schema.json +8613 -0
@@ -0,0 +1,221 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import Ajv2020 from 'ajv/dist/2020.js';
3
+ import parseSpdx from 'spdx-expression-parse';
4
+
5
+ const names = ['common', 'world', 'extension', 'screen-type', 'ipc'];
6
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
7
+ const base = 'https://hyprune.com/schema/v0/';
8
+ for (const name of names) {
9
+ ajv.addSchema(JSON.parse(readFileSync(new URL(`../v0/${name}.schema.json`, import.meta.url))));
10
+ }
11
+ // Versioned directories (v0.N) hold a world format and/or an IPC version; discover
12
+ // them rather than keeping a hand list, so a new pinned schema can't be forgotten.
13
+ const root = new URL('../', import.meta.url);
14
+ const byVersion = (a, b) => { const [x, y] = [a, b].map(v => v.split('.').map(Number)); return x[0] - y[0] || x[1] - y[1]; };
15
+ const versions = readdirSync(root).filter(d => /^v0\.\d+$/.test(d)).map(d => d.slice(1)).sort(byVersion);
16
+ const has = (version, name) => existsSync(new URL(`v${version}/${name}.schema.json`, root));
17
+ for (const v of versions) for (const name of ['common', 'world', 'ipc'])
18
+ if (has(v, name)) ajv.addSchema(JSON.parse(readFileSync(new URL(`v${v}/${name}.schema.json`, root))));
19
+ const validators = new Map();
20
+
21
+ // Contract versions are dotted numbers; compare numerically, never by list.
22
+ export function atLeast(version, minimum) {
23
+ const a = version.split('.').map(Number), b = minimum.split('.').map(Number);
24
+ for (let i = 0; i < Math.max(a.length, b.length); i++)
25
+ if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0);
26
+ return true;
27
+ }
28
+ // World formats: every v0.N directory with a world schema (0.1 uses v0).
29
+ export const worldFormats = ['0.1', ...versions.filter(v => has(v, 'world'))];
30
+ // IPC protocols: every v0.N directory with an IPC schema (0.1 uses v0). Contract
31
+ // versions are all of them (AGENTS rule 6: no hand-kept version lists).
32
+ export const ipcProtocols = ['0.1', ...versions.filter(v => has(v, 'ipc'))];
33
+ export const contractVersions = ['0.1', ...versions];
34
+ // glTF extensions a package may require, by world format (RFC-0004).
35
+ export function sceneExtensions(version) {
36
+ const base = ['KHR_materials_unlit','KHR_texture_transform','KHR_materials_emissive_strength'];
37
+ return atLeast(version, '0.7') ? [...base,'KHR_texture_basisu','EXT_meshopt_compression','KHR_mesh_quantization'] : base;
38
+ }
39
+ // scene.gltf rules that depend on the world format: extensionsRequired, and
40
+ // lightmap textures (world.lightmaps[].texture) stay PNG in every v0 format.
41
+ function validateScene(gltf, version, world) {
42
+ const errors = [], fail = m => errors.push(m);
43
+ if (!worldFormats.includes(version)) throw new Error(`Unknown world format: ${version}`);
44
+ const allowed = new Set(sceneExtensions(version)), required = new Set(gltf.extensionsRequired || []);
45
+ for (const id of required) if (!allowed.has(id)) fail(`extensionsRequired: ${id} is not allowed in world ${version}`);
46
+ const textures = gltf.textures || [], images = gltf.images || [];
47
+ textures.forEach((texture, i) => {
48
+ if (!texture.extensions?.KHR_texture_basisu) return;
49
+ if (!allowed.has('KHR_texture_basisu')) fail(`textures/${i}: KHR_texture_basisu needs world 0.7`);
50
+ else if (!required.has('KHR_texture_basisu')) fail('extensionsRequired: KHR_texture_basisu must be listed when textures use it');
51
+ const image = images[texture.extensions.KHR_texture_basisu.source];
52
+ if (!image || (image.mimeType && image.mimeType !== 'image/ktx2') || (image.uri && !/\.ktx2$/i.test(image.uri)))
53
+ fail(`textures/${i}: KHR_texture_basisu source must be a KTX2 image`);
54
+ });
55
+ if ((gltf.bufferViews || []).some(v => v.extensions?.EXT_meshopt_compression) && !required.has('EXT_meshopt_compression'))
56
+ fail(allowed.has('EXT_meshopt_compression') ? 'extensionsRequired: EXT_meshopt_compression must be listed when buffer views use it'
57
+ : 'bufferViews: EXT_meshopt_compression needs world 0.7');
58
+ for (const lightmap of world?.lightmaps || []) {
59
+ const texture = textures[lightmap.texture];
60
+ if (!texture) { fail(`lightmaps: texture ${lightmap.texture} missing from scene`); continue; }
61
+ const image = images[texture.source];
62
+ if (texture.extensions?.KHR_texture_basisu || !image || (image.mimeType && image.mimeType !== 'image/png') || (image.uri && !/\.png$/i.test(image.uri)))
63
+ fail(`lightmaps: texture ${lightmap.texture} must be a PNG image (KTX2 lightmaps are not allowed in v0)`);
64
+ }
65
+ return { valid: errors.length === 0, errors };
66
+ }
67
+
68
+ // The optional method binds a response to the caller's outstanding request.
69
+ // An envelope alone cannot know whether a valid result belongs to that method.
70
+ export function validate(kind, value, method, version = '0.1', options = {}) {
71
+ if (kind === 'scene') {
72
+ if (method) throw new Error('Method context is only valid for IPC');
73
+ return validateScene(value, version, options.world);
74
+ }
75
+ if (!names.includes(kind) || kind === 'common') throw new Error(`Unknown schema: ${kind}`);
76
+ if (!contractVersions.includes(version)) throw new Error(`Unknown contract: ${version}`);
77
+ const key = `${version}:${kind}:${method || ''}`;
78
+ if (method && kind !== 'ipc') throw new Error('Method context is only valid for IPC');
79
+ if (!validators.has(key)) {
80
+ const directory = kind === 'ipc' && version !== '0.1' ? `v${version}` : kind === 'world' && worldFormats.includes(version) && version !== '0.1' ? `v${version}` : 'v0';
81
+ let schema = `https://hyprune.com/schema/${directory}/${kind}.schema.json`;
82
+ if (method) {
83
+ if (!ajv.getSchema(`${schema}#/$defs/${method}.result`)) throw new Error(`Unknown method: ${method}`);
84
+ schema += `#/$defs/${method}.response`;
85
+ }
86
+ validators.set(key, ajv.getSchema(schema));
87
+ }
88
+ const check = validators.get(key);
89
+ if (!check(value)) return { valid: false, errors: check.errors.map(e => `${e.instancePath || '/'} ${e.message}`) };
90
+ const errors = [];
91
+ const fail = message => errors.push(message);
92
+ const unique = (items, field, label) => {
93
+ const ids = items.map(v => v[field]);
94
+ if (new Set(ids).size !== ids.length) fail(`${label}: duplicate ${field}`);
95
+ };
96
+ const quat = (q, label) => {
97
+ if (Math.abs(Math.hypot(...q) - 1) > 0.001) fail(`${label}: quaternion must be normalized`);
98
+ };
99
+ const license = (text, label) => {
100
+ try { parseSpdx(text); } catch { fail(`${label}: invalid SPDX expression`); }
101
+ };
102
+ if (value.version) {
103
+ const prerelease = value.version.split('+')[0].split('-').slice(1).join('-');
104
+ if (prerelease.split('.').some(p => /^0\d+$/.test(p))) fail('version: numeric prerelease identifiers cannot have leading zeros');
105
+ }
106
+ if (value.license) license(value.license, 'license');
107
+ if (value.requiredExtensions) for (const id of value.requiredExtensions) {
108
+ if (!Object.hasOwn(value.extensions || {}, id)) fail(`requiredExtensions: missing ${id}`);
109
+ }
110
+ if (kind === 'world') {
111
+ const declarations = [value.playable, ...(value.areas || []).map(a=>a.playable)].filter(Boolean);
112
+ if ((value.areas || []).reduce((n,a)=>n+(a.playable?.volumes?.length || (a.playable ? 1 : 0)),0)>64) fail('too many area playable volumes');
113
+ const sub=(a,b)=>a.map((v,i)=>v-b[i]), dot=(a,b)=>a.reduce((n,v,i)=>n+v*b[i],0);
114
+ const cross=(a,b)=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]];
115
+ for(const d of declarations) for(const v of d.volumes || []) {
116
+ if(v.type==='box' && v.min.some((x,i)=>x>=v.max[i])) fail('playable box bounds inverted');
117
+ if(v.type==='convex') {
118
+ const p=v.points; let solid=false;
119
+ for(let i=1;i<p.length;i++)for(let j=i+1;j<p.length;j++)for(let k=j+1;k<p.length;k++)
120
+ if(Math.abs(dot(sub(p[i],p[0]),cross(sub(p[j],p[0]),sub(p[k],p[0]))))>1e-5)solid=true;
121
+ if(!solid)fail('playable convex hull must have nonzero volume');
122
+ }
123
+ }
124
+ const areas=value.visibility?.areas || [];
125
+ unique(areas,'id','visibility areas');
126
+ const areaIds=new Set(areas.map(a=>a.id)), nodes=new Set();
127
+ for(const area of areas){
128
+ if(area.bounds.min.some((v,i)=>v>=area.bounds.max[i]))fail('visibility bounds inverted');
129
+ for(const id of area.visibleFrom)if(!areaIds.has(id))fail('unknown visibility area');
130
+ for(const id of area.nodes){if(nodes.has(id))fail('duplicate visibility node');nodes.add(id);}
131
+ }
132
+
133
+ for (const name of ['spawns', 'zones', 'anchors', 'audio']) unique(value[name] || [], 'id', name);
134
+ if (!value.spawns.some(s => s.id === value.defaultSpawn)) fail('defaultSpawn: unknown spawn');
135
+ for (const entry of [...value.spawns, ...(value.anchors || [])]) quat(entry.pose.rotation, entry.id);
136
+ for (const zone of value.zones || []) if (zone.bounds.min.some((n, i) => n >= zone.bounds.max[i])) fail(`${zone.id}: bounds min must be below max`);
137
+ unique(value.lightmaps || [], 'material', 'lightmaps');
138
+ if (atLeast(value.formatVersion, '0.3')) {
139
+ unique(value.areas || [], 'id', 'areas'); unique(value.areas || [], 'workspaceSlot', 'areas');
140
+ const anchors = new Map((value.anchors || []).map(a => [a.id, a]));
141
+ unique(value.dataObjects || [], 'id', 'dataObjects');
142
+ for(const o of value.dataObjects || []) if(o.mount.role !== 'data-object' || o.mount.interaction !== 'none') fail(`object ${o.id}: role/interaction mismatch`);
143
+ const objects = new Map((value.dataObjects || []).map(a => [a.id, a]));
144
+ const zones = new Map((value.placementZones || []).map(a => [a.id, a]));
145
+ const owned = new Set();
146
+ for (const area of value.areas || []) {
147
+ for (const [role, ids] of [['mounted-workspace', [area.primaryAnchor]], ['tool-mount', area.toolAnchors], ['data-object', area.dataAnchors]]) {
148
+ for (const id of ids) {
149
+ if (owned.has(id)) fail(`anchor ${id}: multiple area owners`); owned.add(id);
150
+ if (role === 'data-object' && objects.has(id)) {
151
+ if (objects.get(id).areaId !== area.id) fail(`object ${id}: area mismatch`);
152
+ } else if (anchors.get(id)?.mount?.role !== role) fail(`anchor ${id}: missing or wrong role`);
153
+ }
154
+ }
155
+ for (const id of area.freePlacementZones || []) if (!zones.has(id)) fail(`zone ${id}: unknown placement volume`);
156
+ if (area.spawnId && !value.spawns.some(s => s.id === area.spawnId)) fail(`area ${area.id}: unknown spawn`);
157
+ }
158
+ for (const a of value.anchors || []) if (a.mount) {
159
+ if (!owned.has(a.id)) fail(`anchor ${a.id}: no area owner`);
160
+ const expected = {'mounted-workspace':'focus-in','tool-mount':'in-place','data-object':'none'}[a.mount.role];
161
+ if (a.mount.interaction !== expected) fail(`anchor ${a.id}: incompatible interaction`);
162
+ const l=a.mount.limits;
163
+ if (l && (l.minAspect>l.maxAspect || l.minSize.some((v,i)=>v>l.maxSize[i]))) fail(`anchor ${a.id}: inverted limits`);
164
+ }
165
+ for (const zone of zones.values()) if (zone.bounds.min.some((v,i)=>v>=zone.bounds.max[i])) fail(`zone ${zone.id}: inverted bounds`);
166
+ }
167
+ unique(value.climbables || [], 'id', 'climbables');
168
+ for (const c of value.climbables || []) {
169
+ const axis = c.top.map((v,i) => v-c.bottom[i]);
170
+ const length = Math.hypot(...axis);
171
+ if (length < .5 || length > 100 || axis[1]/length < .25) fail(`${c.id}: invalid ascending segment`);
172
+ if (Math.abs(Math.hypot(...c.facing)-1) > .001 || Math.abs(axis.reduce((v,n,i)=>v+n*c.facing[i],0)/length) > .001) fail(`${c.id}: facing must be unit and perpendicular to segment`);
173
+ for (const end of ['top','bottom']) if (Math.hypot(...c.exits[end].map((v,i)=>v-c[end][i])) > 3) fail(`${c.id}: exit too far from segment`);
174
+ }
175
+ unique(value.attribution, 'path', 'attribution');
176
+ for (const a of value.attribution) license(a.license, `attribution ${a.path}`);
177
+ }
178
+ if (kind === 'extension') {
179
+ for (const [kind, ids] of Object.entries(value.contributes)) {
180
+ if (kind !== 'screenTypes' && ids.some(id => !id.startsWith(value.id + '.'))) fail(`${kind}: contribution IDs must be beneath extension ID`);
181
+ }
182
+ }
183
+ if (kind === 'ipc') {
184
+ const transition = value.method === 'location.transition' ? value.params : value.result?.snapshot?.state?.location?.transition;
185
+ if (transition) {
186
+ if ((transition.phase === 'loading') !== (transition.progress === null)) fail('only loading uses indeterminate progress');
187
+ if (transition.phase === 'arrived' && transition.progress !== 1) fail('arrival progress must be 1');
188
+ }
189
+ if ((value.method === 'camera.set' || value.method === 'camera.photo')) quat(value.params.orientation, 'camera');
190
+ if (method === 'camera.get' && value.result) quat(value.result.orientation, 'camera');
191
+ if (value.method === 'state.delta' && value.params.revision <= value.params.baseRevision) fail('delta revision must advance');
192
+ if (value.method === 'shell.overlay' && !value.params.visible && value.params.exclusiveInput) fail('hidden overlays cannot claim exclusive input');
193
+ if (value.error && value.id === null && ![-32700, -32600].includes(value.error.code)) fail('null error ID is only allowed for malformed requests');
194
+ const state = value.result?.snapshot?.state;
195
+ const changes = value.method === 'state.delta' ? value.params.changes : undefined;
196
+ for (const s of [state, changes].filter(Boolean)) {
197
+ if (s.movement) {
198
+ quat(s.movement.orientation, 'movement');
199
+ if (version === '0.11' && ((s.movement.state === 'climbing') !== (typeof s.movement.ladderId === 'string'))) fail('climbing requires ladderId; other states require null');
200
+ }
201
+ if (s.surfaces) unique(s.surfaces, 'id', 'surfaces');
202
+ if (s.shell) {
203
+ unique(s.shell.leases, 'id', 'leases');
204
+ const claimed = new Set();
205
+ for (const lease of s.shell.leases) {
206
+ for (const output of lease.outputs) {
207
+ if (claimed.has(output)) fail('output claimed by multiple leases');
208
+ claimed.add(output);
209
+ if (s.runtime && !s.runtime.outputs.includes(output)) fail('lease refers to unknown output');
210
+ }
211
+ unique(lease.overlays, 'outputId', 'overlays');
212
+ for (const overlay of lease.overlays) {
213
+ if (!lease.outputs.includes(overlay.outputId)) fail('overlay output is outside its lease');
214
+ if (!overlay.visible && overlay.exclusiveInput) fail('hidden overlays cannot claim exclusive input');
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ return { valid: errors.length === 0, errors };
221
+ }
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@hyprune/schema",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.0",
4
+ "description": "Versioned JSON Schemas for Hyprune world packages and the IPC protocol, with a validator and CLI for world authors",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "hyprune",
9
+ "hyprland",
10
+ "json-schema",
11
+ "world",
12
+ "ipc",
13
+ "validator"
14
+ ],
15
+ "homepage": "https://hyprune.com/reference/schema/",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/hyprune/schema.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/hyprune/schema/issues"
22
+ },
23
+ "engines": {
24
+ "node": ">=22.12.0"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./lib/validate.d.mts",
29
+ "import": "./lib/validate.mjs"
30
+ },
31
+ "./package.json": "./package.json",
32
+ "./*.json": "./*.json"
33
+ },
34
+ "bin": {
35
+ "hyprune-validate": "bin/validate.mjs"
36
+ },
37
+ "files": [
38
+ "bin/",
39
+ "lib/",
40
+ "v0*/",
41
+ "extensions/",
42
+ "examples/",
43
+ "CHANGELOG.md",
44
+ "README.md",
45
+ "LICENSE"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "test": "node --test",
52
+ "validate": "node bin/validate.mjs",
53
+ "prepublishOnly": "npm test"
54
+ },
55
+ "dependencies": {
56
+ "ajv": "8.20.0",
57
+ "spdx-expression-parse": "4.0.0"
58
+ }
59
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://hyprune.com/schema/v0/common.schema.json",
4
+ "title": "Hyprune v0 common",
5
+ "$defs": {
6
+ "id": {
7
+ "type": "string",
8
+ "minLength": 1,
9
+ "maxLength": 128,
10
+ "pattern": "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$"
11
+ },
12
+ "slug": {
13
+ "type": "string",
14
+ "minLength": 1,
15
+ "maxLength": 64,
16
+ "pattern": "^[a-z][a-z0-9-]*$"
17
+ },
18
+ "version": {
19
+ "type": "string",
20
+ "minLength": 1,
21
+ "maxLength": 256,
22
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
23
+ },
24
+ "path": {
25
+ "type": "string",
26
+ "minLength": 1,
27
+ "maxLength": 512,
28
+ "pattern": "^(?!.*(?:^|/)\\.{1,2}(?:/|$))[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*$"
29
+ },
30
+ "pose": {
31
+ "type": "object",
32
+ "properties": {
33
+ "position": {
34
+ "type": "array",
35
+ "items": {
36
+ "type": "number"
37
+ },
38
+ "minItems": 3,
39
+ "maxItems": 3
40
+ },
41
+ "rotation": {
42
+ "type": "array",
43
+ "items": {
44
+ "type": "number"
45
+ },
46
+ "minItems": 4,
47
+ "maxItems": 4
48
+ }
49
+ },
50
+ "required": [
51
+ "position",
52
+ "rotation"
53
+ ],
54
+ "additionalProperties": false
55
+ },
56
+ "index": {
57
+ "type": "integer",
58
+ "minimum": 0
59
+ },
60
+ "capability": {
61
+ "enum": [
62
+ "state.read",
63
+ "world.control",
64
+ "surface.control",
65
+ "movement.control",
66
+ "shell.control",
67
+ "runtime.control"
68
+ ]
69
+ },
70
+ "extensions": {
71
+ "type": "object",
72
+ "propertyNames": {
73
+ "$ref": "common.schema.json#/$defs/id"
74
+ },
75
+ "additionalProperties": {
76
+ "type": "object"
77
+ }
78
+ },
79
+ "revision": {
80
+ "type": "integer",
81
+ "minimum": 0,
82
+ "maximum": 9007199254740991
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,121 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://hyprune.com/schema/v0/extension.schema.json",
4
+ "title": "Hyprune v0 extension",
5
+ "type": "object",
6
+ "properties": {
7
+ "manifestVersion": {
8
+ "const": "0.1"
9
+ },
10
+ "id": {
11
+ "$ref": "common.schema.json#/$defs/id"
12
+ },
13
+ "version": {
14
+ "$ref": "common.schema.json#/$defs/version"
15
+ },
16
+ "name": {
17
+ "type": "string",
18
+ "minLength": 1,
19
+ "maxLength": 256
20
+ },
21
+ "description": {
22
+ "type": "string",
23
+ "minLength": 0,
24
+ "maxLength": 4096
25
+ },
26
+ "license": {
27
+ "type": "string",
28
+ "minLength": 1,
29
+ "maxLength": 256
30
+ },
31
+ "apiVersion": {
32
+ "const": "0.1"
33
+ },
34
+ "runtime": {
35
+ "const": "process"
36
+ },
37
+ "entrypoint": {
38
+ "$ref": "common.schema.json#/$defs/path"
39
+ },
40
+ "capabilities": {
41
+ "type": "array",
42
+ "items": {
43
+ "$ref": "common.schema.json#/$defs/capability"
44
+ },
45
+ "minItems": 0,
46
+ "uniqueItems": true
47
+ },
48
+ "contributes": {
49
+ "type": "object",
50
+ "properties": {
51
+ "screenTypes": {
52
+ "type": "array",
53
+ "items": {
54
+ "$ref": "common.schema.json#/$defs/path"
55
+ },
56
+ "minItems": 0,
57
+ "uniqueItems": true
58
+ },
59
+ "widgets": {
60
+ "type": "array",
61
+ "items": {
62
+ "$ref": "common.schema.json#/$defs/id"
63
+ },
64
+ "minItems": 0,
65
+ "uniqueItems": true
66
+ },
67
+ "movementModes": {
68
+ "type": "array",
69
+ "items": {
70
+ "$ref": "common.schema.json#/$defs/id"
71
+ },
72
+ "minItems": 0,
73
+ "uniqueItems": true
74
+ },
75
+ "integrations": {
76
+ "type": "array",
77
+ "items": {
78
+ "$ref": "common.schema.json#/$defs/id"
79
+ },
80
+ "minItems": 0,
81
+ "uniqueItems": true
82
+ }
83
+ },
84
+ "required": [],
85
+ "additionalProperties": false
86
+ },
87
+ "sandbox": {
88
+ "type": "object",
89
+ "properties": {
90
+ "required": {
91
+ "const": true
92
+ },
93
+ "network": {
94
+ "const": false
95
+ }
96
+ },
97
+ "required": [
98
+ "required",
99
+ "network"
100
+ ],
101
+ "additionalProperties": false
102
+ },
103
+ "extensions": {
104
+ "$ref": "common.schema.json#/$defs/extensions"
105
+ }
106
+ },
107
+ "required": [
108
+ "manifestVersion",
109
+ "id",
110
+ "version",
111
+ "name",
112
+ "license",
113
+ "apiVersion",
114
+ "runtime",
115
+ "entrypoint",
116
+ "capabilities",
117
+ "contributes",
118
+ "sandbox"
119
+ ],
120
+ "additionalProperties": false
121
+ }