openvibers/openvibe.contracts Commits
Contracts 0.410.0: release train for #455, #456 and #457 (#458)
Changes
@@@ -1,5 +1,43 @@# Changelog++## 0.410.0 — 2026-11-11++++A train of three pull requests: the layouts of make-your-own-controls (#455), Elements stage 1 (#456) and the deep dive++(#457).++++- Add `ui.surface@1` (the catalog of a surface's parts, their minimum sizes, styles, bindings and++ `never_covers` rules), `ui.layout@1` (a person's arrangement on one device class, with its name, author and++ sharing), `ui.layout-request@1` (catalog, current layout, words and device for the AI task) and++ `ui.layout-suggestion@1` (a validated layout with a one-sentence summary, reasons and the model's receipt, or++ an honest refusal with the validator's reasons).++- Add `network.layout.changed@1`: the event Network writes in the transaction that sets, saves, renames, deletes,++ shares, publishes, copies or resets one person's layout. It carries the surface, device, layout id, change and++ revision, never account data or layout contents.++- Add `ai.layout.suggest` (first-party, `POST /api/v1/ui/layout/suggest`) and `network.layout.read` (first-party,++ `GET /api/v1/people/:subject/layouts/:surface`), and list them in the AI and Network manifests.++- The fixtures cover a bot-controls catalog, a phone layout, a docked overlay, a suggestion and a refusal, a++ request and the event; the suite checks part ids, theme tokens, never_covers targets and the 44 px minimum. (#455)++- Add `ui.element@1`: a look from OpenVibe.Design's components and the theme's tokens, a recipe (triggers, conditions,++ actions), the capabilities the element declares, an optional mod reference (id, version, digest) and its sharing.++ The store assigns the id and revision; a draft has neither.++- Add `ui.trigger@1` (press, hold, double-press, key, gamepad, chat command and text, follow, tip, AI Moment, Rundown++ topic, timer, schedule, robot telemetry and event, stream start and end, viewer count, another element's event) and++ `ui.action@1` (robot, chat, screen, stream, AI, Actor, webhook and tip-goal actions), each action carrying the product++ that authorises it. `ui.action@1`'s `$defs.capability` is the one list an element's `capabilities` draws from.++- Add `ui.element-state@1`: the element's own store (counters, scores, lists), with a revision that grows per accepted++ change and values that are booleans, numbers, short strings or lists of them, never objects.++- Add `live.element.pressed@1` and `live.element.state@1`: a press with the presser's public name and the trigger, and++ the store with its revision, written by Live.++- Fixtures cover a gallery dance button, a chat counter, a mod element, trigger and action variants, the state, and++ both events; `test/elements.test.js` holds the capability list to the action catalog, each kind to its product, and++ the recipe to the element's declared capabilities. (#456)++- Add `ai.deep-dive@1`: a deep dive run's report as OpenVibe.AI's `design.deep_dive` workflow produces it for++ OpenVibe.Exchange and OpenVibe.Design — the question and depth, the seven steps, every source, every capture (a++ private Media object per viewport and theme, with its DOM facts and Expert extras), the per-site analysis, the++ synthesis, the recommendations checked against OpenVibe's rules, and the receipt. Every claim carries its evidence.++- Add `ai.deep_dive.updated@1`, the event AI writes on each step transition and at the end, so a page can follow a++ run without polling and Design can attach a finished report. (#457)++## 0.409.0 — 2026-11-11- Define machine access requests, grants and signed token claims; the agent directory, A2A tasks and results; five Network events and four registration and relay capabilities.@@@ -5,7 +5,7 @@**Docs:** https://openvibe.services · **For AI:** https://openvibe.services/llms.txt**Install and test:** `npm ci && npm test` (Node 20 or newer)--**Status:** alpha (first released in Wave 1; the release is package.json's `version`, tagged). At v0.408.0: 1238 schemas in `contracts/catalog.json`, 66 service manifests, 516 capability manifests, 14 namespace manifests and the staff capability map. Service repositories run `openvibe-contracts-check` and the pin-drift check in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Consumers pin release tags (each repository's `STATUS.json` names its pin). `node scripts/docs-currency.js <root>` checks every checkout's README and `STATUS.json` against its code: the pins and versions they name, a library's consumer list, the deployed flag against the manifest, README links, and the paths and migrations they name. `node scripts/estate.js` generates `docs/ESTATE.md` and `manifests/repositories/` from the estate checkouts, and `--check` fails on drift (plan T1 step 3).++**Status:** alpha (first released in Wave 1; the release is package.json's `version`, tagged). At v0.410.0: 1261 schemas in `contracts/catalog.json`, 67 service manifests, 523 capability manifests, 14 namespace manifests and the staff capability map. Service repositories run `openvibe-contracts-check` and the pin-drift check in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Consumers pin release tags (each repository's `STATUS.json` names its pin). `node scripts/docs-currency.js <root>` checks every checkout's README and `STATUS.json` against its code: the pins and versions they name, a library's consumer list, the deployed flag against the manifest, README links, and the paths and migrations they name. `node scripts/estate.js` generates `docs/ESTATE.md` and `manifests/repositories/` from the estate checkouts, and `--check` fails on drift (plan T1 step 3).**Plan:** OpenVibe Development Roadmap, Wave 1 (implementation plan rev 3, §3.1 and §18.11).**License:** AGPL-3.0 (same as every OpenVibe service).nnew file mode 100644@@@ -0,0 +1,925 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ai/deep-dive.v1.json",++ "title": "AiDeepDive",++ "description": "A deep dive: expert design research run by OpenVibe.AI (workflow design.deep_dive) for OpenVibe.Exchange's /deep-dive page and, through the API, OpenVibe.Design. One document holds the question and depth (the input), the seven steps, every source read, every capture (a private Media object per viewport and theme, with its DOM facts and Expert extras), the per-site analysis, the synthesis across sites, the recommendations checked against OpenVibe's own rules, and the receipt of every model call and capture. Every claim carries its evidence: the site, the capture or crop, the URL and the date it was read. Nothing is copied: captures are private to the run's owner and the report shows them as attributed commentary.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "schema",++ "run_id",++ "status",++ "input",++ "steps",++ "sources",++ "captures",++ "analysis",++ "synthesis",++ "recommendations",++ "receipt",++ "gaps",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The run id; a deep dive is a run (GET /api/v1/deep-dives/{id})."++ },++ "schema": {++ "const": "ai.deep-dive@1"++ },++ "run_id": {++ "type": "string",++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "status": {++ "enum": [++ "queued",++ "running",++ "choosing",++ "succeeded",++ "failed",++ "cancelled"++ ]++ },++ "input": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "question",++ "depth",++ "max_sites"++ ],++ "properties": {++ "question": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500++ },++ "depth": {++ "enum": [++ "quick",++ "thorough",++ "expert"++ ]++ },++ "max_sites": {++ "type": "integer",++ "minimum": 1,++ "maximum": 30++ },++ "sites": {++ "type": "array",++ "maxItems": 30,++ "items": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "description": "Sites the person named when starting the run."++ }++ }++ },++ "steps": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "key",++ "status"++ ],++ "properties": {++ "key": {++ "enum": [++ "plan",++ "search",++ "choose",++ "capture",++ "analyse",++ "synthesise",++ "report"++ ]++ },++ "status": {++ "enum": [++ "pending",++ "running",++ "done",++ "skipped",++ "failed"++ ]++ },++ "detail": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "finished_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++ }++ },++ "sources": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "kind",++ "title",++ "url",++ "read_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "kind": {++ "enum": [++ "web",++ "picture",++ "icon",++ "project",++ "rules",++ "component",++ "named"++ ]++ },++ "title": {++ "type": "string",++ "maxLength": 400++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "publisher": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200++ },++ "engine": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 80++ },++ "snippet": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "license": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 120++ },++ "read_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ },++ "captures": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "source_id",++ "url",++ "captured_at",++ "viewport",++ "theme",++ "full_page",++ "crops"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^cap_[A-Za-z0-9_-]{1,40}$"++ },++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "final_url": {++ "type": [++ "string",++ "null"++ ],++ "format": "uri",++ "pattern": "^https?://"++ },++ "title": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 400++ },++ "captured_at": {++ "type": "string",++ "format": "date-time"++ },++ "viewport": {++ "enum": [++ 390,++ 1280++ ]++ },++ "theme": {++ "enum": [++ "light",++ "dark"++ ]++ },++ "full_page": {++ "type": "boolean"++ },++ "media_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "width": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "height": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "crops": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "media_id",++ "y",++ "height"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^crop_[A-Za-z0-9_-]{1,40}$"++ },++ "media_id": {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "y": {++ "type": "integer",++ "minimum": 0++ },++ "height": {++ "type": "integer",++ "minimum": 1++ }++ }++ }++ },++ "dom": {++ "type": [++ "object",++ "null"++ ]++ },++ "extras": {++ "type": [++ "object",++ "null"++ ]++ },++ "skip": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200,++ "description": "Why the capture did not happen: robots_disallowed, not_public, no_chrome, timeout, …"++ },++ "cached": {++ "type": "boolean"++ }++ }++ }++ },++ "analysis": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "source_id",++ "url",++ "summary",++ "strengths",++ "weaknesses",++ "evidence"++ ],++ "properties": {++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "summary": {++ "type": "string",++ "maxLength": 4000++ },++ "first_impression": {++ "type": "string",++ "maxLength": 2000++ },++ "hierarchy": {++ "type": "string",++ "maxLength": 2000++ },++ "density": {++ "type": "string",++ "maxLength": 2000++ },++ "type": {++ "type": "string",++ "maxLength": 2000++ },++ "colour": {++ "type": "string",++ "maxLength": 2000++ },++ "motion": {++ "type": "string",++ "maxLength": 2000++ },++ "words": {++ "type": "string",++ "maxLength": 2000++ },++ "strengths": {++ "type": "array",++ "maxItems": 12,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ },++ "weaknesses": {++ "type": "array",++ "maxItems": 12,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ },++ "accessibility": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "speed": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "evidence": {++ "$ref": "#/$defs/evidence"++ }++ }++ }++ },++ "synthesis": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "patterns",++ "comparison",++ "best",++ "mistakes",++ "gap",++ "reference_tokens",++ "do",++ "dont",++ "accessibility",++ "speed"++ ],++ "properties": {++ "patterns": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "name",++ "what",++ "sites",++ "evidence"++ ],++ "properties": {++ "name": {++ "type": "string",++ "maxLength": 160++ },++ "what": {++ "type": "string",++ "maxLength": 1200++ },++ "sites": {++ "type": "array",++ "maxItems": 30,++ "items": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ }++ },++ "usage_count": {++ "type": "integer",++ "minimum": 0,++ "maximum": 30++ },++ "evidence": {++ "$ref": "#/$defs/evidence"++ }++ }++ }++ },++ "comparison": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "columns",++ "rows"++ ],++ "properties": {++ "columns": {++ "type": "array",++ "maxItems": 12,++ "items": {++ "type": "string",++ "maxLength": 120++ }++ },++ "rows": {++ "type": "array",++ "items": {++ "type": "array",++ "maxItems": 12,++ "items": {++ "type": "string",++ "maxLength": 1000++ }++ }++ }++ }++ },++ "best": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "source_id",++ "what",++ "evidence"++ ],++ "properties": {++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "what": {++ "type": "string",++ "maxLength": 2000++ },++ "evidence": {++ "$ref": "#/$defs/evidence"++ }++ }++ },++ "mistakes": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "what",++ "evidence"++ ],++ "properties": {++ "what": {++ "type": "string",++ "maxLength": 1200++ },++ "evidence": {++ "$ref": "#/$defs/evidence"++ }++ }++ }++ },++ "gap": {++ "type": "string",++ "maxLength": 2000++ },++ "reference_tokens": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "palettes",++ "type_scales",++ "spacing"++ ],++ "properties": {++ "palettes": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "source_id",++ "colours"++ ],++ "properties": {++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "colours": {++ "type": "array",++ "maxItems": 16,++ "items": {++ "type": "string",++ "maxLength": 40++ }++ }++ }++ }++ },++ "type_scales": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "source_id",++ "families",++ "sizes"++ ],++ "properties": {++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "families": {++ "type": "array",++ "maxItems": 8,++ "items": {++ "type": "string",++ "maxLength": 120++ }++ },++ "sizes": {++ "type": "array",++ "maxItems": 16,++ "items": {++ "type": "string",++ "maxLength": 40++ }++ }++ }++ }++ },++ "spacing": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "source_id",++ "steps"++ ],++ "properties": {++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "steps": {++ "type": "array",++ "maxItems": 16,++ "items": {++ "type": "string",++ "maxLength": 40++ }++ }++ }++ }++ }++ }++ },++ "do": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ },++ "dont": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ },++ "accessibility": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ },++ "speed": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ }++ }++ },++ "recommendations": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "what",++ "why",++ "rule",++ "evidence"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^rec_[A-Za-z0-9_-]{1,40}$"++ },++ "what": {++ "type": "string",++ "maxLength": 1200++ },++ "why": {++ "type": "string",++ "maxLength": 2000++ },++ "rule": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "status",++ "text"++ ],++ "properties": {++ "id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 120++ },++ "status": {++ "enum": [++ "follows",++ "would_change",++ "none"++ ]++ },++ "text": {++ "type": "string",++ "maxLength": 600++ }++ }++ },++ "evidence": {++ "$ref": "#/$defs/evidence"++ }++ }++ }++ },++ "receipt": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "price_usd",++ "held_usd",++ "cost_usd",++ "models",++ "captures_count",++ "synthetic"++ ],++ "properties": {++ "price_usd": {++ "type": "number",++ "minimum": 0++ },++ "held_usd": {++ "type": "number",++ "minimum": 0++ },++ "cost_usd": {++ "type": "number",++ "minimum": 0++ },++ "captures_count": {++ "type": "integer",++ "minimum": 0++ },++ "synthetic": {++ "type": "boolean"++ },++ "models": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "step",++ "provider",++ "model",++ "tokens_in",++ "tokens_out",++ "cost_usd",++ "ms"++ ],++ "properties": {++ "step": {++ "enum": [++ "plan",++ "choose",++ "analyse",++ "synthesise"++ ]++ },++ "provider": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 80++ },++ "model": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200++ },++ "tokens_in": {++ "type": "integer",++ "minimum": 0++ },++ "tokens_out": {++ "type": "integer",++ "minimum": 0++ },++ "cost_usd": {++ "type": "number",++ "minimum": 0++ },++ "ms": {++ "type": "integer",++ "minimum": 0++ }++ }++ }++ }++ }++ },++ "gaps": {++ "type": "array",++ "maxItems": 40,++ "items": {++ "type": "string",++ "maxLength": 400++ }++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ },++ "finished_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ },++ "$defs": {++ "evidence": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "claim",++ "url",++ "read_at"++ ],++ "properties": {++ "claim": {++ "type": "string",++ "maxLength": 600++ },++ "source_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "capture_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^cap_[A-Za-z0-9_-]{1,40}$"++ },++ "crop_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^crop_[A-Za-z0-9_-]{1,40}$"++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "read_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ }++ }++}@@@ -11573,6 +11573,125 @@"visibility": "first-party","compatibility": "backward","status": "active"++ },++ {++ "id": "ui.surface",++ "version": "1.0.0",++ "owner": "contracts",++ "schema": "ui/surface.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "ui.layout",++ "version": "1.0.0",++ "owner": "contracts",++ "schema": "ui/layout.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "ui.layout-request",++ "version": "1.0.0",++ "owner": "contracts",++ "schema": "ui/layout-request.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "ui.layout-suggestion",++ "version": "1.0.0",++ "owner": "contracts",++ "schema": "ui/layout-suggestion.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "network.layout.changed",++ "version": "1.0.0",++ "owner": "network",++ "schema": "events/payloads/network.layout.changed.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "ui.action",++ "version": "1.0.0",++ "owner": "contracts",++ "schema": "ui/action.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "ui.element",++ "version": "1.0.0",++ "owner": "contracts",++ "schema": "ui/element.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "ui.element-state",++ "version": "1.0.0",++ "owner": "contracts",++ "schema": "ui/element-state.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "ui.trigger",++ "version": "1.0.0",++ "owner": "contracts",++ "schema": "ui/trigger.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "live.element.pressed",++ "version": "1.0.0",++ "owner": "live",++ "schema": "events/payloads/live.element.pressed.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "live.element.state",++ "version": "1.0.0",++ "owner": "live",++ "schema": "events/payloads/live.element.state.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "ai.deep-dive",++ "version": "1.0.0",++ "owner": "ai",++ "schema": "ai/deep-dive.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active",++ "adr": "ADR-015"++ },++ {++ "id": "ai.deep_dive.updated",++ "version": "1.0.0",++ "owner": "ai",++ "schema": "events/payloads/ai.deep_dive.updated.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active",++ "adr": "ADR-015"}]}nnew file mode 100644@@@ -0,0 +1,147 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/ai.deep_dive.updated.v1.json",++ "title": "AiDeepDiveUpdatedPayload",++ "description": "ai.deep_dive.updated v1 (OpenVibe.AI server/deep-dive): a deep dive's step changed or it ended. One event per step transition and one at the end, so a page (OpenVibe.Exchange's /deep-dive) can follow a run without polling, and OpenVibe.Design can attach a finished report. The payload carries the run and deep-dive ids, the step, the progress and the status; never the question's answer, a capture or a prompt. Envelope: subject { type: run, id: <run_id> }, visibility internal, priority low (important on failed), actor service:ai.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "deep_dive_id",++ "run_id",++ "status",++ "step",++ "progress",++ "steps",++ "depth",++ "requester",++ "on_behalf_of",++ "url",++ "error",++ "occurred_at"++ ],++ "properties": {++ "deep_dive_id": {++ "type": "string",++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "run_id": {++ "type": "string",++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "status": {++ "enum": [++ "queued",++ "running",++ "choosing",++ "succeeded",++ "failed",++ "cancelled"++ ]++ },++ "step": {++ "enum": [++ "plan",++ "search",++ "choose",++ "capture",++ "analyse",++ "synthesise",++ "report",++ "done",++ null++ ]++ },++ "progress": {++ "type": "number",++ "minimum": 0,++ "maximum": 1++ },++ "steps": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "key",++ "status"++ ],++ "properties": {++ "key": {++ "enum": [++ "plan",++ "search",++ "choose",++ "capture",++ "analyse",++ "synthesise",++ "report"++ ]++ },++ "status": {++ "enum": [++ "pending",++ "running",++ "done",++ "skipped",++ "failed"++ ]++ }++ }++ }++ },++ "depth": {++ "enum": [++ "quick",++ "thorough",++ "expert"++ ]++ },++ "requester": {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ "on_behalf_of": {++ "anyOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ]++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://"++ },++ "error": {++ "anyOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "code",++ "detail"++ ],++ "properties": {++ "code": {++ "type": "string",++ "maxLength": 80++ },++ "detail": {++ "type": "string",++ "maxLength": 500++ }++ }++ },++ {++ "type": "null"++ }++ ]++ },++ "occurred_at": {++ "type": "string",++ "format": "date-time"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,29 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/live.element.pressed.v1.json",++ "title": "LiveElementPressedPayload",++ "description": "live.element.pressed v1 (OpenVibe.Live server/elements): a viewer's element press was accepted and ran. It carries the public facts the stream should see: the channel, the element's id and name, the presser's public name and subject, the trigger that fired, the channel points it cost and the element-state revision it left behind. It never carries the presser's email, address, account balance or anything private; a refused press does not produce it. Envelope: subject { type: element, id: el_… }, actor the presser's subject, visibility public, priority low. Also written to the channel's AI timeline as a moment, so the stream's own timeline can cite 'chat made the robot dance'.",++ "type": "object",++ "additionalProperties": false,++ "required": ["channel", "element_id", "element_name", "presser", "presser_name", "trigger", "at"],++ "properties": {++ "channel": {++ "type": "object",++ "additionalProperties": false,++ "required": ["username", "display_name"],++ "properties": {++ "username": { "type": "string", "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$" },++ "display_name": { "type": "string", "minLength": 1, "maxLength": 80 }++ },++ "description": "The public channel facts (the channel page is public already)."++ },++ "element_id": { "type": "string", "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$" },++ "element_name": { "type": "string", "minLength": 1, "maxLength": 80 },++ "presser": { "$ref": "../../identity/subject-ref.v1.json" },++ "presser_name": { "type": "string", "minLength": 1, "maxLength": 120, "description": "The display name the stream shows, never an email or an account id." },++ "trigger": { "$ref": "../../ui/trigger.v1.json" },++ "points_spent": { "type": "integer", "minimum": 0, "description": "Channel points the press cost; 0 when it was free." },++ "state_revision": { "type": "integer", "minimum": 0, "description": "The element-state revision after the actions ran; 0 when the element has no state yet." },++ "at": { "type": "string", "format": "date-time" }++ }++}nnew file mode 100644@@@ -0,0 +1,51 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/live.element.state.v1.json",++ "title": "LiveElementStatePayload",++ "description": "live.element.state v1 (OpenVibe.Live server/elements): an element's own store changed, broadcast to everyone watching through Live's existing events path. Carries the channel's public facts and the same store as ui.element-state@1: the element instance, a revision that grows by one per accepted change, the values themselves and when. It is public state (a counter, a score, a wheel's entries), never the presser's data: a value may not be an object, a markup string or a URL, and the payload carries no account.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "channel",++ "element_id",++ "revision",++ "values",++ "updated_at"++ ],++ "properties": {++ "channel": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "username",++ "display_name"++ ],++ "properties": {++ "username": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$"++ },++ "display_name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ }++ }++ },++ "element_id": {++ "type": "string",++ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 0++ },++ "values": {++ "$ref": "../../ui/element-state.v1.json#/$defs/values"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/network.layout.changed.v1.json",++ "title": "NetworkLayoutChangedPayload",++ "description": "network.layout.changed v1 (OpenVibe.Network server/layouts, written into network_event_outbox in the transaction that changes a stored layout). One event per change to one person's ui.layout@1: set active, saved, renamed, deleted, shared, unshared, published to the gallery, unpublished, copied from the gallery, or reset to the default. Envelope: subject { type: user, id } and the layout's revision, actor the person, visibility internal. The payload carries no account data and no layout contents, so an open page can announce a change and refetch with the person's own token.",++ "type": "object",++ "additionalProperties": false,++ "required": ["surface", "device", "layout_id", "change", "revision"],++ "properties": {++ "surface": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",++ "maxLength": 80++ },++ "device": { "enum": ["phone", "tablet", "computer"] },++ "layout_id": { "type": "string", "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$" },++ "change": {++ "enum": ["active", "saved", "renamed", "deleted", "shared", "unshared", "published", "unpublished", "copied", "reset"]++ },++ "revision": { "type": "integer", "minimum": 1 },++ "sharing": { "enum": ["private", "link", "gallery"] }++ }++}nnew file mode 100644@@@ -0,0 +1,702 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/action.v1.json",++ "title": "UiAction",++ "description": "ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop, the e-stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.",++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "direction",++ "ms"++ ],++ "description": "Drive the robot for a moment, then stop. The duration is bounded by Bot's own max_command_ms and the owner's limits for the presser's role.",++ "properties": {++ "kind": {++ "const": "robot.drive_pulse"++ },++ "product": {++ "const": "bot"++ },++ "direction": {++ "enum": [++ "forward",++ "back",++ "left",++ "right",++ "spin_left",++ "spin_right"++ ]++ },++ "ms": {++ "type": "integer",++ "minimum": 50,++ "maximum": 3000++ },++ "speed": {++ "type": "number",++ "minimum": 0.1,++ "maximum": 1,++ "description": "Fraction of the robot's allowed speed; omitted means the owner's default for the role."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "preset"++ ],++ "description": "Take the robot's head to a preset. Bot maps up, center and down to the head actuator its robot profile declares; a robot without one refuses it.",++ "properties": {++ "kind": {++ "const": "robot.head_preset"++ },++ "product": {++ "const": "bot"++ },++ "preset": {++ "enum": [++ "up",++ "center",++ "down"++ ],++ "description": "A head tilt preset. Bot still checks the robot profile and the owner's limits."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "on"++ ],++ "description": "Turn the robot's headlight on or off. Bot's own headlight kind is the authority: only the owner and People Operators may send it, and a robot whose profile has no headlight refuses it.",++ "properties": {++ "kind": {++ "const": "robot.headlight"++ },++ "product": {++ "const": "bot"++ },++ "on": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "text"++ ],++ "description": "Speak a line through the robot. Text goes through the existing content checks and Bot's own speak limits.",++ "properties": {++ "kind": {++ "const": "robot.say"++ },++ "product": {++ "const": "bot"++ },++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "macro"++ ],++ "description": "Replay a recorded robot macro through the safe command path, with its own deadline. Macros are stage 4 of Elements; a stage-1 product refuses this kind as not yet supported.",++ "properties": {++ "kind": {++ "const": "robot.macro"++ },++ "product": {++ "const": "bot"++ },++ "macro": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "text"++ ],++ "description": "Send a chat message as the channel's bot. Text goes through Chat's content checks and rate limits.",++ "properties": {++ "kind": {++ "const": "chat.message"++ },++ "product": {++ "const": "chat"++ },++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 400++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "source"++ ],++ "properties": {++ "kind": {++ "const": "chat.pin"++ },++ "product": {++ "const": "chat"++ },++ "source": {++ "enum": [++ "trigger",++ "latest"++ ],++ "description": "trigger pins the message that caused this action; latest pins the newest chat message."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "question",++ "options"++ ],++ "properties": {++ "kind": {++ "const": "chat.poll"++ },++ "product": {++ "const": "chat"++ },++ "question": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "options": {++ "type": "array",++ "minItems": 2,++ "maxItems": 10,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ }++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "title"++ ],++ "properties": {++ "kind": {++ "const": "screen.alert"++ },++ "product": {++ "const": "live"++ },++ "title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "name"++ ],++ "properties": {++ "kind": {++ "const": "screen.animation"++ },++ "product": {++ "const": "live"++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "An animation the page or overlay has; an unknown name is refused, never fetched."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product"++ ],++ "properties": {++ "kind": {++ "const": "screen.confetti"++ },++ "product": {++ "const": "live"++ },++ "intensity": {++ "enum": [++ "low",++ "medium",++ "high"++ ]++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "sound"++ ],++ "properties": {++ "kind": {++ "const": "screen.sound"++ },++ "product": {++ "const": "live"++ },++ "sound": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "A sound from the streamer's own sound list; Live never fetches a URL for it."++ },++ "volume": {++ "type": "number",++ "minimum": 0,++ "maximum": 1++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "text",++ "voice"++ ],++ "properties": {++ "kind": {++ "const": "screen.tts"++ },++ "product": {++ "const": "live"++ },++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300++ },++ "voice": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "A voice from the streamer's own voice list."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "key",++ "by"++ ],++ "description": "Move one of the element's counters. The host applies it to the element's own state and Live broadcasts the new state.",++ "properties": {++ "kind": {++ "const": "screen.counter"++ },++ "product": {++ "const": "live"++ },++ "key": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{0,31}$"++ },++ "by": {++ "type": "integer",++ "minimum": -1000,++ "maximum": 1000++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "key",++ "value"++ ],++ "description": "Set one of the element's state values: a boolean, a number, a short string or a list of those (a wheel's entries, a scoreboard). Objects are not state values.",++ "properties": {++ "kind": {++ "const": "screen.state"++ },++ "product": {++ "const": "live"++ },++ "key": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{0,31}$"++ },++ "value": {++ "oneOf": [++ {++ "type": "boolean"++ },++ {++ "type": "number"++ },++ {++ "type": "string",++ "maxLength": 200++ },++ {++ "type": "array",++ "maxItems": 64,++ "items": {++ "oneOf": [++ {++ "type": "number"++ },++ {++ "type": "string",++ "maxLength": 200++ }++ ]++ }++ }++ ]++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "element",++ "visible"++ ],++ "properties": {++ "kind": {++ "const": "screen.visibility"++ },++ "product": {++ "const": "live"++ },++ "element": {++ "type": "string",++ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "visible": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "scene"++ ],++ "properties": {++ "kind": {++ "const": "screen.scene"++ },++ "product": {++ "const": "live"++ },++ "scene": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "A scene the layout has; an unknown scene is refused."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product"++ ],++ "properties": {++ "kind": {++ "const": "stream.clip"++ },++ "product": {++ "const": "live"++ },++ "seconds": {++ "type": "integer",++ "minimum": 5,++ "maximum": 30,++ "description": "How far back the clip starts, at most 30 seconds."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product"++ ],++ "properties": {++ "kind": {++ "const": "stream.moment"++ },++ "product": {++ "const": "live"++ },++ "label": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "title"++ ],++ "properties": {++ "kind": {++ "const": "stream.title"++ },++ "product": {++ "const": "live"++ },++ "title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 140++ }++ },++ "description": "Change the stream title. Live allows it only for the channel's owner, whatever the element's trigger says."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "category"++ ],++ "properties": {++ "kind": {++ "const": "stream.category"++ },++ "product": {++ "const": "live"++ },++ "category": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ }++ },++ "description": "Change the stream category. Live allows it only for the channel's owner."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "prompt"++ ],++ "description": "Ask an AI model through Exchange's catalogue, with the element's budget and a receipt. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",++ "properties": {++ "kind": {++ "const": "ai.ask"++ },++ "product": {++ "const": "ai"++ },++ "prompt": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500++ },++ "model": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "max_cost_usd": {++ "type": "number",++ "minimum": 0,++ "maximum": 1++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "task"++ ],++ "description": "Hand a task to an Actor agent. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",++ "properties": {++ "kind": {++ "const": "actor.task"++ },++ "product": {++ "const": "actor"++ },++ "task": {++ "type": "string",++ "minLength": 1,++ "maxLength": 8000++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "endpoint"++ ],++ "description": "Call one of the streamer's own webhook endpoints. The element names the endpoint the streamer listed; it never carries a URL, and the payload never carries a viewer's data. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",++ "properties": {++ "kind": {++ "const": "webhook.call"++ },++ "product": {++ "const": "live"++ },++ "endpoint": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_-]{0,31}$"++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "goal"++ ],++ "description": "Update a tip goal's display. Money never moves here; Billing's own sheet confirms any charge. Stage 4 of Elements.",++ "properties": {++ "kind": {++ "const": "money.tip_goal"++ },++ "product": {++ "const": "billing"++ },++ "goal": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40++ }++ }++ }++ ],++ "$defs": {++ "capability": {++ "description": "The action kinds an Element may declare in ui.element@1 capabilities. One id, one product: the host routes an action only to the product that owns it.",++ "enum": [++ "robot.drive_pulse",++ "robot.head_preset",++ "robot.headlight",++ "robot.say",++ "robot.macro",++ "chat.message",++ "chat.pin",++ "chat.poll",++ "screen.alert",++ "screen.animation",++ "screen.confetti",++ "screen.sound",++ "screen.tts",++ "screen.counter",++ "screen.state",++ "screen.visibility",++ "screen.scene",++ "stream.clip",++ "stream.moment",++ "stream.title",++ "stream.category",++ "ai.ask",++ "actor.task",++ "webhook.call",++ "money.tip_goal"++ ]++ }++ }++}nnew file mode 100644@@@ -0,0 +1,52 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/element-state.v1.json",++ "title": "UiElementState",++ "description": "ui.element-state@1: the small store an Element keeps for itself (counters, scores, lists, a wheel's entries), as Live broadcasts it to everyone watching and as a read returns it. CHANNEL and ELEMENT_ID name the element instance, REVISION grows by one with each accepted change so a late frame never overwrites a newer one, and VALUES is the store itself: at most 32 keys, each a boolean, a number, a short string or a list of those. It is state, not code: no objects, no markup, no URL and no account data, which keeps a shared element's state safe to render for every viewer.",++ "type": "object",++ "additionalProperties": false,++ "required": ["channel", "element_id", "revision", "values", "updated_at"],++ "properties": {++ "channel": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$",++ "description": "The Live channel's username (its slug)."++ },++ "element_id": {++ "type": "string",++ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 0++ },++ "values": { "$ref": "#/$defs/values" },++ "updated_at": { "type": "string", "format": "date-time" }++ },++ "$defs": {++ "values": {++ "type": "object",++ "maxProperties": 32,++ "propertyNames": { "pattern": "^[a-z][a-z0-9_]{0,31}$" },++ "additionalProperties": { "$ref": "#/$defs/value" },++ "description": "The element's own store. A key is lower-case and 32 characters at most; a value is a boolean, a number, a string of 200 characters at most or a list of those."++ },++ "value": {++ "oneOf": [++ { "type": "boolean" },++ { "type": "number" },++ { "type": "string", "maxLength": 200 },++ {++ "type": "array",++ "maxItems": 64,++ "items": {++ "oneOf": [++ { "type": "number" },++ { "type": "string", "maxLength": 200 }++ ]++ }++ }++ ]++ }++ }++}nnew file mode 100644@@@ -0,0 +1,203 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/element.v1.json",++ "title": "UiElement",++ "description": "ui.element@1: an Element people make and place on a surface: a look, a behaviour and, only when a recipe is not enough, a mod. LOOK draws from OpenVibe.Design's components and the theme's tokens, so it follows the person's theme, the seasons and dark/light; it carries no markup and no URL. RECIPE is data only: one or more triggers from ui.trigger@1, the conditions that decide who may fire it and what it costs, and one or more actions from ui.action@1, checked against CAPABILITIES. A capability is an action kind the element declares; the host refuses an action the element did not declare, and the product named by the action still checks the presser's role, the cooldown, the cost and its own limits. MOD is an optional reference to a code module (id, version and digest) for the stage where a recipe is not enough; the module never adds a power. Conditions use channel points only in stage 1: no Vibes, no money, and Billing still confirms any charge itself. The id and revision are assigned by the store; a draft has neither.",++ "type": "object",++ "additionalProperties": false,++ "required": ["name", "author", "sharing", "look", "recipe", "capabilities"],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Assigned by the store when the element is saved; absent in a draft."++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "description": "Assigned by the store; every saved change moves it by one."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "description": {++ "type": "string",++ "maxLength": 500++ },++ "author": {++ "type": "object",++ "additionalProperties": false,++ "required": ["name"],++ "properties": {++ "name": { "type": "string", "minLength": 1, "maxLength": 80 },++ "subject": { "$ref": "../identity/subject-ref.v1.json" }++ },++ "description": "The author's display name and, for a private or link element, the subject. A gallery or copy answer carries the display name only, never the account."++ },++ "sharing": { "enum": ["private", "link", "gallery"] },++ "look": {++ "type": "object",++ "additionalProperties": false,++ "required": ["component", "label"],++ "properties": {++ "component": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,63}$",++ "description": "An OpenVibe.Design component name (design.component@1), e.g. button or meter. A name the page does not have is refused, never fetched."++ },++ "label": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "The words on the element, e.g. Make it dance."++ },++ "icon": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,31}$",++ "description": "A glyph name from the design icon set."++ },++ "props": {++ "type": "object",++ "maxProperties": 20,++ "propertyNames": { "pattern": "^[a-z][a-zA-Z0-9]{0,31}$" },++ "additionalProperties": {++ "oneOf": [++ { "type": "boolean" },++ { "type": "number" },++ { "type": "string", "maxLength": 200 }++ ]++ },++ "description": "The design component's own props (the sliders the editor shows). Numbers, strings and booleans only; no nesting."++ },++ "style": { "$ref": "#/$defs/style" }++ }++ },++ "recipe": {++ "type": "object",++ "additionalProperties": false,++ "required": ["triggers", "actions"],++ "properties": {++ "triggers": {++ "type": "array",++ "minItems": 1,++ "maxItems": 8,++ "items": { "$ref": "trigger.v1.json" }++ },++ "conditions": { "$ref": "#/$defs/conditions" },++ "actions": {++ "type": "array",++ "minItems": 1,++ "maxItems": 16,++ "items": { "$ref": "action.v1.json" }++ }++ }++ },++ "capabilities": {++ "type": "array",++ "minItems": 1,++ "maxItems": 32,++ "uniqueItems": true,++ "items": { "$ref": "action.v1.json#/$defs/capability" },++ "description": "The action kinds this element may request. Every action in the recipe must be declared here; the host refuses the rest."++ },++ "initial_state": {++ "$ref": "element-state.v1.json#/$defs/values",++ "description": "The element's state before anything runs; the counter starts at 0, never at a value the element did not declare."++ },++ "mod": {++ "type": "object",++ "additionalProperties": false,++ "required": ["id", "version", "sha256"],++ "properties": {++ "id": { "type": "string", "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$" },++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$"++ },++ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },++ "capabilities": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": { "$ref": "action.v1.json#/$defs/capability" },++ "description": "The subset of the element's capabilities the module itself may request; omitted means the element's declared capabilities, and a product may grant fewer."++ }++ },++ "description": "A code module for an element whose recipe is not enough (stage 3). It runs sandboxed; the reference is data, and the host checks every request the module makes against the element's capabilities."++ },++ "created_at": { "type": "string", "format": "date-time" },++ "updated_at": { "type": "string", "format": "date-time" }++ },++ "$defs": {++ "style": {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "tint": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,39}$",++ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."++ },++ "shape": { "enum": ["round", "pill", "square"] },++ "opacity": { "type": "number", "minimum": 0.4, "maximum": 1 },++ "show_label": { "type": "boolean" }++ }++ },++ "conditions": {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "role": {++ "enum": ["everyone", "follower", "subscriber", "vip", "operator", "owner"],++ "description": "Who may fire it. A higher role admits the lower ones where the product says so (an operator is admitted to subscriber-only); owner is the channel's owner alone. Default everyone."++ },++ "cost": {++ "type": "object",++ "additionalProperties": false,++ "required": ["points"],++ "properties": {++ "points": { "type": "integer", "minimum": 1, "maximum": 1000000 }++ },++ "description": "Channel points the presser spends, from Live's points. Stage 1 has no other currency, and an element never charges money."++ },++ "cooldown": {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "per_person_seconds": { "type": "integer", "minimum": 0, "maximum": 86400 },++ "overall_seconds": { "type": "integer", "minimum": 0, "maximum": 86400 }++ },++ "description": "How long the same person must wait, and how long everybody must wait, before it can fire again."++ },++ "daily_cap": {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "per_person": { "type": "integer", "minimum": 1, "maximum": 10000 },++ "overall": { "type": "integer", "minimum": 1, "maximum": 1000000 }++ },++ "description": "The most times it may fire in a UTC day, per person and for everyone."++ },++ "live_only": {++ "type": "boolean",++ "description": "When true the element fires only while the channel is live."++ },++ "robot_turn_only": {++ "type": "boolean",++ "description": "When true a robot action is requested only while the presser has the robot's turn; Bot still decides."++ },++ "chance": {++ "type": "object",++ "additionalProperties": false,++ "required": ["one_in"],++ "properties": {++ "one_in": { "type": "integer", "minimum": 1, "maximum": 1000000 }++ },++ "description": "Fire only one time in N. The host draws the lot, so a refused draw costs nothing."++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,26 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/layout-request.v1.json",++ "title": "UiLayoutRequest",++ "description": "ui.layout-request@1: what an editor sends OpenVibe.AI's ui.layout.suggest task, and what Network forwards for a signed-in person. It is the surface's catalog, the layout shown now (or null for the default one) and the words the person typed (max 500 characters), for one device class. The catalog is the only power the answer can use; the current layout is the starting arrangement; the words never contain markup that becomes part of it.",++ "type": "object",++ "additionalProperties": false,++ "required": ["catalog", "layout", "words", "device"],++ "properties": {++ "catalog": { "$ref": "surface.v1.json" },++ "layout": {++ "oneOf": [++ { "$ref": "layout.v1.json" },++ { "type": "null" }++ ],++ "description": "The arrangement shown now, or null when the person is looking at the default."++ },++ "words": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500,++ "description": "The person's own description, e.g. \"put Stop on the left and make it huge\"."++ },++ "device": { "enum": ["phone", "tablet", "computer"] }++ }++}nnew file mode 100644@@@ -0,0 +1,54 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/layout-suggestion.v1.json",++ "title": "UiLayoutSuggestion",++ "description": "ui.layout-suggestion@1: OpenVibe.AI's answer to ui.layout.suggest. ok true carries the layout, already checked by openvibe-sdk/ui-layout against the catalog sent with the request, and a one-sentence summary of what changed plus the model's reasons. ok false is an honest refusal: no layout, one sentence saying what it could not do, and the validator's reasons in plain words (for example \"Stop has to stay visible.\"). A receipt names the model that answered, with the run and the route. The suggestion is previewed, never applied silently; Network or the product stores it only after the person accepts it.",++ "type": "object",++ "additionalProperties": false,++ "required": ["ok", "layout", "summary", "reasons"],++ "properties": {++ "ok": { "type": "boolean" },++ "layout": {++ "oneOf": [++ { "$ref": "layout.v1.json" },++ { "type": "null" }++ ],++ "description": "The validated layout when ok is true; null when it is false."++ },++ "summary": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300,++ "description": "One sentence saying what changed, or what could not be done."++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "items": { "type": "string", "minLength": 1, "maxLength": 300 },++ "description": "Why this arrangement, or the validator's reasons for refusing one."++ },++ "receipt": {++ "type": "object",++ "additionalProperties": false,++ "required": ["model"],++ "properties": {++ "model": { "type": "string", "minLength": 1, "maxLength": 200 },++ "provider": { "type": "string", "minLength": 1, "maxLength": 100 },++ "route": { "type": "string", "minLength": 1, "maxLength": 200 },++ "run_id": { "type": "string", "minLength": 1, "maxLength": 80 },++ "cost_usd": { "type": "number", "minimum": 0 }++ },++ "description": "Which model answered: the same receipt every AI run carries, at the task's own shape."++ }++ },++ "allOf": [++ {++ "if": { "properties": { "ok": { "const": true } }, "required": ["ok"] },++ "then": { "properties": { "layout": { "type": "object" } } }++ },++ {++ "if": { "properties": { "ok": { "const": false } }, "required": ["ok"] },++ "then": { "properties": { "layout": { "type": "null" } } }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,256 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/layout.v1.json",++ "title": "UiLayout",++ "description": "ui.layout@1: one person's arrangement of one surface on one device class. A layout names the surface and its catalog version, the device, its own name, its author and its sharing (private, link or gallery), and one entry per part it places. A part is visible or not, sits on the surface's grid (x, y, w, h in cells) or docks to an edge for an overlay, and carries a style (a theme token tint, a shape, opacity 0.4-1 and the label) and bindings. It is data only: no URL, no markup and no code may appear anywhere in it, and every part id, tint, shape, opacity, size and binding must be one the catalog allows. openvibe-sdk/ui-layout validates it before any server, editor or AI answer stores or shows it.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "surface",++ "surface_version",++ "device",++ "name",++ "author",++ "sharing",++ "parts"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Assigned by OpenVibe.Network when the layout is stored; absent in a draft, a share answer or an AI suggestion."++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "description": "Assigned by the store; every change to a stored layout moves it by one."++ },++ "active": {++ "type": "boolean",++ "description": "Set by OpenVibe.Network: the one layout a product draws for this surface on this device class. A draft, a share answer and an AI suggestion carry no active field."++ },++ "surface": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",++ "maxLength": 80++ },++ "surface_version": {++ "type": "integer",++ "minimum": 1++ },++ "device": {++ "enum": [++ "phone",++ "tablet",++ "computer"++ ]++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "The person's name for this layout; \"My phone controls\" is fine."++ },++ "author": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "name"++ ],++ "properties": {++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "subject": {++ "$ref": "../identity/subject-ref.v1.json"++ }++ },++ "description": "The author's display name and, for a private or link layout, the subject. A share or gallery answer carries the display name only, never the account."++ },++ "sharing": {++ "enum": [++ "private",++ "link",++ "gallery"++ ]++ },++ "parts": {++ "type": "array",++ "minItems": 1,++ "maxItems": 64,++ "items": {++ "$ref": "#/$defs/part"++ }++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "$defs": {++ "place": {++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "x",++ "y",++ "w",++ "h"++ ],++ "properties": {++ "x": {++ "type": "integer",++ "minimum": 0,++ "maximum": 23++ },++ "y": {++ "type": "integer",++ "minimum": 0,++ "maximum": 100++ },++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ }++ },++ "description": "The part's cell on the surface grid."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "dock",++ "order",++ "w",++ "h"++ ],++ "properties": {++ "dock": {++ "enum": [++ "left",++ "right",++ "top",++ "bottom"++ ]++ },++ "order": {++ "type": "integer",++ "minimum": 0,++ "maximum": 63++ },++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ }++ },++ "description": "For an overlay: the edge the part docks to, its order along that edge, and its size in cells."++ }++ ]++ },++ "style": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "tint",++ "shape",++ "opacity",++ "label"++ ],++ "properties": {++ "tint": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,39}$",++ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."++ },++ "shape": {++ "enum": [++ "round",++ "pill",++ "square"++ ]++ },++ "opacity": {++ "type": "number",++ "minimum": 0.4,++ "maximum": 1++ },++ "label": {++ "type": "boolean"++ }++ }++ },++ "bindings": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "keys",++ "gamepad"++ ],++ "properties": {++ "keys": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"++ }++ },++ "gamepad": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ }++ }++ }++ },++ "part": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "visible",++ "place",++ "style",++ "bindings"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ },++ "visible": {++ "type": "boolean"++ },++ "place": {++ "$ref": "#/$defs/place"++ },++ "style": {++ "$ref": "#/$defs/style"++ },++ "bindings": {++ "$ref": "#/$defs/bindings"++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,200 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/surface.v1.json",++ "title": "UiSurface",++ "description": "ui.surface@1: the catalog a product publishes for one surface people may arrange (bot.controls, bot.overlay, live.channel, dock, chat.composer, navbar.shortcuts). It names the surface, its device classes and grid, the theme tokens its styles draw from, and every part a layout may place. A part says what it is (stick, button, slider, readout, video, list), whether it is required or pressed, its minimum cells and the minimum pixels the product guarantees (44 px for anything pressed, larger for Stop), the sizes and styles it allows, the bindings it accepts, and the parts it must never cover. A layout is validated against this catalog (openvibe-sdk/ui-layout); a layout can arrange only what the catalog has and can never add a power. The catalog is data: a layout or an AI answer never carries code or a URL.",++ "type": "object",++ "additionalProperties": false,++ "required": ["id", "version", "name", "devices", "grid", "theme_tokens", "parts"],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",++ "maxLength": 80,++ "description": "The surface id: <product>.<surface>, e.g. bot.controls."++ },++ "version": {++ "type": "integer",++ "minimum": 1,++ "description": "The catalog version. A layout names the version it was made against."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "What the surface is called where a person sees it."++ },++ "description": {++ "type": "string",++ "maxLength": 500++ },++ "devices": {++ "type": "array",++ "minItems": 1,++ "maxItems": 3,++ "uniqueItems": true,++ "items": { "enum": ["phone", "tablet", "computer"] },++ "description": "The device classes this surface offers."++ },++ "grid": {++ "type": "object",++ "additionalProperties": false,++ "required": ["columns", "cell_px"],++ "properties": {++ "columns": {++ "type": "object",++ "additionalProperties": false,++ "required": ["phone", "tablet", "computer"],++ "description": "The grid's columns per device class: 4 on a phone and 12 on a computer in the design; a product may choose otherwise within these bounds.",++ "properties": {++ "phone": { "type": "integer", "minimum": 2, "maximum": 8 },++ "tablet": { "type": "integer", "minimum": 2, "maximum": 16 },++ "computer": { "type": "integer", "minimum": 2, "maximum": 24 }++ }++ },++ "cell_px": {++ "type": "object",++ "additionalProperties": false,++ "required": ["phone", "tablet", "computer"],++ "description": "The smallest a cell is rendered at that device class, in CSS pixels, at the narrowest supported width. The validator multiplies a part's cells by this to check the 44 px rule.",++ "properties": {++ "phone": { "type": "integer", "minimum": 24, "maximum": 200 },++ "tablet": { "type": "integer", "minimum": 24, "maximum": 200 },++ "computer": { "type": "integer", "minimum": 24, "maximum": 200 }++ }++ }++ }++ },++ "theme_tokens": {++ "type": "array",++ "minItems": 1,++ "maxItems": 64,++ "uniqueItems": true,++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,39}$" },++ "description": "The theme tokens a tint may name (accent, surface, danger, ...). Never a colour value: the theme decides what the token is."++ },++ "parts": {++ "type": "array",++ "minItems": 1,++ "maxItems": 64,++ "items": { "$ref": "#/$defs/part" }++ }++ },++ "$defs": {++ "part": {++ "type": "object",++ "additionalProperties": false,++ "required": ["id", "name", "kind", "required", "pressed", "min", "styles", "bindings"],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$",++ "description": "Stable part id inside this catalog, e.g. stop, stick, battery."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "kind": { "enum": ["stick", "button", "slider", "readout", "video", "list"] },++ "required": {++ "type": "boolean",++ "description": "A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused."++ },++ "pressed": {++ "type": "boolean",++ "description": "The part sends a command when pressed or moved; it must render at least 44 px on every side."++ },++ "min": {++ "type": "object",++ "additionalProperties": false,++ "required": ["w", "h"],++ "description": "The smallest size a layout may give the part: w and h in grid cells, and px when the product guarantees more than 44 px (Stop).",++ "properties": {++ "w": { "type": "integer", "minimum": 1, "maximum": 24 },++ "h": { "type": "integer", "minimum": 1, "maximum": 24 },++ "px": { "type": "integer", "minimum": 1, "maximum": 400 }++ }++ },++ "sizes": {++ "type": "array",++ "maxItems": 12,++ "uniqueItems": true,++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": ["w", "h"],++ "properties": {++ "w": { "type": "integer", "minimum": 1, "maximum": 24 },++ "h": { "type": "integer", "minimum": 1, "maximum": 24 }++ }++ },++ "description": "Sizes the editor may offer as presets. A layout is not limited to these: it is limited to min and to the grid."++ },++ "styles": {++ "type": "object",++ "additionalProperties": false,++ "required": ["tints", "shapes"],++ "properties": {++ "tints": {++ "type": "array",++ "minItems": 1,++ "maxItems": 16,++ "uniqueItems": true,++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,39}$" },++ "description": "The theme tokens this part may be tinted with; every one is also in the surface's theme_tokens."++ },++ "shapes": {++ "type": "array",++ "minItems": 1,++ "maxItems": 3,++ "uniqueItems": true,++ "items": { "enum": ["round", "pill", "square"] }++ },++ "opacity": {++ "type": "object",++ "additionalProperties": false,++ "required": ["min", "max"],++ "properties": {++ "min": { "type": "number", "minimum": 0.4, "maximum": 1 },++ "max": { "type": "number", "minimum": 0.4, "maximum": 1 }++ },++ "description": "The opacity range this part allows; the design's range is 0.4 to 1."++ },++ "label": {++ "type": "boolean",++ "description": "Whether the editor may show the part's label on it."++ }++ }++ },++ "bindings": {++ "type": "object",++ "additionalProperties": false,++ "required": ["keys", "gamepad"],++ "properties": {++ "keys": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": { "type": "string", "pattern": "^[A-Za-z0-9+_.-]{1,40}$" }++ },++ "gamepad": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]{0,39}$" }++ }++ },++ "description": "The bindings the product accepts for this part. A layout may pick a subset, never add one."++ },++ "never_covers": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]{0,39}$" },++ "description": "The parts this part must never cover, in addition to the surface-wide rule that nothing covers a required part."++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,202 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/trigger.v1.json",++ "title": "UiTrigger",++ "description": "ui.trigger@1: one trigger an Element's recipe listens for, from the fixed catalog. kind says what happened: a viewer pressing, holding or double-pressing the element, a key or gamepad binding, a chat command or text, a follow, a tip, an AI Moment, a Rundown topic, a timer or schedule, robot telemetry or an event, the stream starting or ending, a viewer-count crossing or another element's event. The fields a kind needs are the only fields it accepts. A trigger is data: it never names code or a URL, and a product may refuse a kind it does not offer yet. Stage 1 of Elements uses press and chat_command; the rest are declared here so a stage-2 element and the editor speak one language.",++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind"],++ "properties": {++ "kind": { "const": "press" }++ },++ "description": "The element itself was pressed. This is the Viewer Deck's button."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "ms"],++ "properties": {++ "kind": { "const": "hold" },++ "ms": { "type": "integer", "minimum": 300, "maximum": 10000 }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind"],++ "properties": {++ "kind": { "const": "double_press" },++ "within_ms": { "type": "integer", "minimum": 100, "maximum": 2000, "description": "The longest gap between the two presses; 400 by default." }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "key"],++ "properties": {++ "kind": { "const": "key" },++ "key": { "type": "string", "pattern": "^[A-Za-z0-9+_.-]{1,40}$" },++ "modifiers": {++ "type": "array",++ "maxItems": 4,++ "uniqueItems": true,++ "items": { "enum": ["ctrl", "alt", "shift", "meta"] }++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "button"],++ "properties": {++ "kind": { "const": "gamepad" },++ "button": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]{0,39}$" },++ "index": { "type": "integer", "minimum": 0, "maximum": 7, "description": "Which connected pad; omitted means any." }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "command"],++ "properties": {++ "kind": { "const": "chat_command" },++ "command": { "type": "string", "pattern": "^![a-z0-9_-]{1,32}$", "description": "The command without arguments, lower case, e.g. !dance." }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "words"],++ "properties": {++ "kind": { "const": "chat_text" },++ "words": {++ "type": "array",++ "minItems": 1,++ "maxItems": 16,++ "uniqueItems": true,++ "items": { "type": "string", "minLength": 1, "maxLength": 40 }++ },++ "mode": { "enum": ["any", "all"], "description": "any (default) matches one of the words; all matches every one." }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind"],++ "properties": {++ "kind": { "const": "follow" }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind"],++ "properties": {++ "kind": { "const": "tip" },++ "min": { "type": "integer", "minimum": 0, "description": "The smallest tip, in Vibes, that fires the trigger; omitted means any tip." }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind"],++ "properties": {++ "kind": { "const": "ai_moment" }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind"],++ "properties": {++ "kind": { "const": "rundown_topic" },++ "words": {++ "type": "array",++ "maxItems": 8,++ "uniqueItems": true,++ "items": { "type": "string", "minLength": 1, "maxLength": 40 },++ "description": "Optional words the topic has to mention; omitted means any topic."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "every_seconds"],++ "properties": {++ "kind": { "const": "timer" },++ "every_seconds": { "type": "integer", "minimum": 5, "maximum": 86400 },++ "start": { "enum": ["live", "always"], "description": "live (default) counts only while the channel is live; always counts from the element being active." }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "cron", "timezone"],++ "properties": {++ "kind": { "const": "schedule" },++ "cron": { "type": "string", "pattern": "^[0-9*/, -]{9,80}$", "description": "A five-field cron in the person's timezone." },++ "timezone": { "type": "string", "minLength": 1, "maxLength": 64 }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "metric", "op", "value"],++ "properties": {++ "kind": { "const": "robot_telemetry" },++ "metric": { "enum": ["battery_percent", "latency_ms", "stuck", "temperature_c"] },++ "op": { "enum": ["lt", "gt"] },++ "value": { "type": "number" },++ "for_seconds": { "type": "integer", "minimum": 0, "maximum": 3600, "description": "How long the condition must hold; 0 (default) means at once." }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "event"],++ "properties": {++ "kind": { "const": "robot_event" },++ "event": { "enum": ["estop", "stopped", "disconnected", "command_refused", "button"] }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind"],++ "properties": {++ "kind": { "const": "stream_start" }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind"],++ "properties": {++ "kind": { "const": "stream_end" }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "op", "count"],++ "properties": {++ "kind": { "const": "viewer_count" },++ "op": { "enum": ["above", "below"] },++ "count": { "type": "integer", "minimum": 0, "maximum": 100000000 }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": ["kind", "element_id"],++ "properties": {++ "kind": { "const": "element_event" },++ "element_id": { "type": "string", "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$" },++ "event": { "enum": ["pressed", "state", "shown"], "description": "pressed (default) fires after that element ran; state and shown fire when Live broadcasts them." }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,28 @@++{++ "id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "schema": "ai.deep-dive@1",++ "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "status": "finished",++ "input": { "question": "a phone-first robot control panel", "depth": "quick", "max_sites": 6 },++ "steps": [],++ "sources": [],++ "captures": [],++ "analysis": [],++ "synthesis": {++ "patterns": [],++ "comparison": { "columns": [], "rows": [] },++ "best": { "source_id": "src_1", "what": "x", "evidence": [] },++ "mistakes": [],++ "gap": "",++ "reference_tokens": { "palettes": [], "type_scales": [], "spacing": [] },++ "do": [],++ "dont": [],++ "accessibility": [],++ "speed": []++ },++ "recommendations": [],++ "receipt": { "price_usd": 0.2, "held_usd": 0.2, "cost_usd": 0, "models": [], "captures_count": 0, "synthetic": false },++ "gaps": [],++ "created_at": "2026-11-11T10:00:00.000Z",++ "updated_at": "2026-11-11T10:03:21.000Z"++}nnew file mode 100644@@@ -0,0 +1,139 @@++{++ "id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "schema": "ai.deep-dive@1",++ "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "status": "succeeded",++ "input": {++ "question": "a phone-first robot control panel",++ "depth": "quick",++ "max_sites": 6,++ "sites": ["https://example.com/robot"]++ },++ "steps": [++ { "key": "plan", "status": "done", "detail": "wrote 9 expert questions", "started_at": "2026-11-11T10:00:00.000Z", "finished_at": "2026-11-11T10:00:02.000Z" },++ { "key": "search", "status": "done", "detail": "4 engines, 12 sources", "started_at": "2026-11-11T10:00:02.000Z", "finished_at": "2026-11-11T10:00:40.000Z" },++ { "key": "choose", "status": "done", "detail": "6 sites chosen", "started_at": "2026-11-11T10:00:40.000Z", "finished_at": "2026-11-11T10:00:42.000Z" },++ { "key": "capture", "status": "done", "detail": "12 captures", "started_at": "2026-11-11T10:00:42.000Z", "finished_at": "2026-11-11T10:02:30.000Z" },++ { "key": "analyse", "status": "done", "detail": "6 sites read", "started_at": "2026-11-11T10:02:30.000Z", "finished_at": "2026-11-11T10:03:05.000Z" },++ { "key": "synthesise", "status": "done", "detail": "8 patterns", "started_at": "2026-11-11T10:03:05.000Z", "finished_at": "2026-11-11T10:03:20.000Z" },++ { "key": "report", "status": "done", "detail": "report ready", "started_at": "2026-11-11T10:03:20.000Z", "finished_at": "2026-11-11T10:03:21.000Z" }++ ],++ "sources": [++ {++ "id": "src_1",++ "kind": "web",++ "title": "A phone-first robot control panel",++ "url": "https://example.com/robot",++ "publisher": "example.com",++ "engine": "openvibe",++ "snippet": "A control panel laid out for one thumb.",++ "license": null,++ "read_at": "2026-11-11T10:00:10.000Z"++ }++ ],++ "captures": [++ {++ "id": "cap_1",++ "source_id": "src_1",++ "url": "https://example.com/robot",++ "final_url": "https://example.com/robot",++ "title": "A phone-first robot control panel",++ "captured_at": "2026-11-11T10:00:45.000Z",++ "viewport": 390,++ "theme": "light",++ "full_page": true,++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "width": 390,++ "height": 2400,++ "crops": [++ { "id": "crop_1", "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPR", "y": 0, "height": 844 },++ { "id": "crop_2", "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS", "y": 844, "height": 844 }++ ],++ "dom": { "fonts": { "families": ["Inter"], "sizes": ["14px", "16px", "28px"] }, "colours": [{ "value": "rgb(12, 14, 18)", "share": 0.71 }] },++ "extras": null,++ "skip": null,++ "cached": false++ }++ ],++ "analysis": [++ {++ "source_id": "src_1",++ "url": "https://example.com/robot",++ "summary": "One thumb reaches every primary control; the live camera fills the first screen.",++ "first_impression": "The robot's camera frame is the page.",++ "hierarchy": "One headline, one live frame, then the controls.",++ "density": "Spacious; four controls per screen.",++ "type": "One family, three sizes.",++ "colour": "Near-black on white with a single amber action.",++ "motion": "The frame pulses when connected.",++ "words": "Verbs on buttons, state in the status line.",++ "strengths": ["Every control sits in the bottom third", "State is written, not only coloured"],++ "weaknesses": ["The emergency stop is the same size as the rest"],++ "accessibility": "The stop button has no accessible name.",++ "speed": "The first frame arrived once the socket opened; no skeleton.",++ "evidence": [++ { "claim": "Controls sit in the bottom third", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_2", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" },++ { "claim": "The stop button has no accessible name", "source_id": "src_1", "capture_id": "cap_1", "crop_id": null, "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }++ ]++ }++ ],++ "synthesis": {++ "patterns": [++ {++ "name": "Live view first",++ "what": "The camera or state view is the whole first screen, controls below it.",++ "sites": ["src_1"],++ "usage_count": 1,++ "evidence": [{ "claim": "The frame is the first screen", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_1", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }]++ }++ ],++ "comparison": {++ "columns": ["Site", "First screen", "Controls"],++ "rows": [["example.com", "live frame", "bottom third"]]++ },++ "best": {++ "source_id": "src_1",++ "what": "The live frame doubles as the connection state.",++ "evidence": [{ "claim": "The frame carries the status line", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_1", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }]++ },++ "mistakes": [++ { "what": "The emergency stop is not separated from the other controls.", "evidence": [{ "claim": "All buttons share one row", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_2", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }] }++ ],++ "gap": "No control panel shows what the robot is about to do before the tap is sent.",++ "reference_tokens": {++ "palettes": [{ "source_id": "src_1", "colours": ["#0c0e12", "#ffffff", "#f5a623"] }],++ "type_scales": [{ "source_id": "src_1", "families": ["Inter"], "sizes": ["14px", "16px", "28px"] }],++ "spacing": [{ "source_id": "src_1", "steps": ["4px", "8px", "16px", "24px"] }]++ },++ "do": ["Keep every primary control in the thumb reach of one hand"],++ "dont": ["Do not make the stop control share the row of ordinary actions"],++ "accessibility": ["Name every icon-only control"],++ "speed": ["Show the frame before the socket finishes connecting"]++ },++ "recommendations": [++ {++ "id": "rec_1",++ "what": "Put the emergency stop alone, full width, at the bottom.",++ "why": "The best panel suggests it by layout; the failure here is that it does not.",++ "rule": { "id": "control.destructive-separation", "status": "follows", "text": "Destructive actions are separated from ordinary ones." },++ "evidence": [{ "claim": "The stop shares the row", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_2", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }]++ }++ ],++ "receipt": {++ "price_usd": 0.2,++ "held_usd": 0.2,++ "cost_usd": 0.014,++ "captures_count": 12,++ "synthetic": false,++ "models": [++ { "step": "plan", "provider": "deepseek", "model": "deepseek-v4-flash", "tokens_in": 900, "tokens_out": 320, "cost_usd": 0.0004, "ms": 1800 },++ { "step": "choose", "provider": "deepseek", "model": "deepseek-v4-flash", "tokens_in": 2400, "tokens_out": 500, "cost_usd": 0.0007, "ms": 2100 },++ { "step": "analyse", "provider": "deepseek", "model": "deepseek-v4-pro", "tokens_in": 12000, "tokens_out": 4000, "cost_usd": 0.012, "ms": 26000 },++ { "step": "synthesise", "provider": "deepseek", "model": "deepseek-v4-pro", "tokens_in": 6000, "tokens_out": 2500, "cost_usd": 0.007, "ms": 12000 }++ ]++ },++ "gaps": ["No Expert extras were measured at Quick depth."],++ "created_at": "2026-11-11T10:00:00.000Z",++ "updated_at": "2026-11-11T10:03:21.000Z",++ "finished_at": "2026-11-11T10:03:21.000Z"++}nnew file mode 100644@@@ -0,0 +1,14 @@++{++ "deep_dive_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "status": "running",++ "step": "capture",++ "progress": 4.5,++ "steps": [],++ "depth": "quick",++ "requester": { "type": "service", "id": "exchange" },++ "on_behalf_of": null,++ "url": "https://openvibe.exchange/deep-dive/run_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "error": null,++ "occurred_at": "2026-11-11T10:01:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "deep_dive_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "status": "running",++ "step": "capture",++ "progress": 0.5,++ "steps": [++ { "key": "plan", "status": "done" },++ { "key": "search", "status": "done" },++ { "key": "choose", "status": "done" },++ { "key": "capture", "status": "running" },++ { "key": "analyse", "status": "pending" },++ { "key": "synthesise", "status": "pending" },++ { "key": "report", "status": "pending" }++ ],++ "depth": "quick",++ "requester": { "type": "service", "id": "exchange" },++ "on_behalf_of": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" },++ "url": "https://openvibe.exchange/deep-dive/run_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "error": null,++ "occurred_at": "2026-11-11T10:01:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "channel": {++ "username": "ada",++ "display_name": "Ada"++ },++ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "element_name": "Make it dance",++ "presser": {++ "type": "user",++ "id": "alice"++ },++ "presser_name": "Alice",++ "trigger": {++ "kind": "press"++ },++ "at": "2026-11-11T20:31:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,15 @@++{++ "channel": {++ "username": "ada",++ "display_name": "Ada"++ },++ "presser": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR"++ },++ "presser_name": "Maticus",++ "trigger": {++ "kind": "press"++ },++ "at": "2026-11-11T20:31:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,20 @@++{++ "channel": {++ "username": "ada",++ "display_name": "Ada"++ },++ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPS",++ "element_name": "Headlight tally",++ "presser": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR"++ },++ "presser_name": "Maticus",++ "trigger": {++ "kind": "chat_command",++ "command": "!headlights"++ },++ "points_spent": 0,++ "state_revision": 2,++ "at": "2026-11-11T20:31:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "channel": {++ "username": "ada",++ "display_name": "Ada"++ },++ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "element_name": "Make it dance",++ "presser": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR"++ },++ "presser_name": "Maticus",++ "trigger": {++ "kind": "press"++ },++ "points_spent": 50,++ "state_revision": 4,++ "at": "2026-11-11T20:31:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "channel": {++ "username": "ada",++ "display_name": "Ada"++ },++ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "revision": 1,++ "values": {++ "wheel": {++ "entries": [++ "a"++ ]++ }++ },++ "updated_at": "2026-11-11T20:31:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "channel": {++ "username": "ada",++ "display_name": "Ada"++ },++ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "revision": 4,++ "values": {++ "dances": 3,++ "last_presser": "Maticus",++ "wheel": [++ "a",++ "b",++ 3++ ],++ "lit": true++ },++ "updated_at": "2026-11-11T20:31:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "surface": "bot.controls",++ "device": "phone",++ "layout_id": "lay_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "change": "applied",++ "revision": 3++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "surface": "bot.controls",++ "device": "phone",++ "layout_id": "lay_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "change": "active",++ "revision": 3,++ "sharing": "private"++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "surface": "bot.controls",++ "device": "computer",++ "layout_id": "lay_01JAB2C3D4E5F6G7H8J9K0MNPR",++ "change": "published",++ "revision": 7,++ "sharing": "gallery"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "kind": "robot.say",++ "product": "bot"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "kind": "robot.dance",++ "product": "bot"++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "kind": "robot.drive_pulse",++ "product": "live",++ "direction": "forward",++ "ms": 600++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "kind": "chat.message",++ "product": "chat",++ "text": "Nice one!"++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "kind": "robot.drive_pulse",++ "product": "bot",++ "direction": "forward",++ "ms": 600++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "kind": "screen.alert",++ "product": "live",++ "title": "Dance!",++ "text": "Ada made the robot dance."++}nnew file mode 100644@@@ -0,0 +1,13 @@++{++ "channel": "ada",++ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "revision": 1,++ "values": {++ "wheel": {++ "entries": [++ "a"++ ]++ }++ },++ "updated_at": "2026-11-11T20:31:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,9 @@++{++ "channel": "ada",++ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "revision": 1,++ "values": {++ "Dances": 1++ },++ "updated_at": "2026-11-11T20:31:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "channel": "ada",++ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "revision": 4,++ "values": {++ "dances": 3,++ "last_presser": "Maticus",++ "wheel": [++ "a",++ "b",++ 3++ ],++ "lit": true++ },++ "updated_at": "2026-11-11T20:31:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "name": "Bad",++ "author": {++ "name": "Ada"++ },++ "sharing": "private",++ "look": {++ "component": "button",++ "label": "Bad"++ },++ "recipe": {++ "triggers": [++ {++ "kind": "press"++ }++ ]++ },++ "capabilities": [++ "robot.say"++ ]++}nnew file mode 100644@@@ -0,0 +1,35 @@++{++ "name": "Bad",++ "author": {++ "name": "Ada"++ },++ "sharing": "private",++ "look": {++ "component": "button",++ "label": "Bad"++ },++ "recipe": {++ "triggers": [++ {++ "kind": "press"++ }++ ],++ "actions": [++ {++ "kind": "robot.say",++ "product": "bot",++ "text": "hi"++ }++ ]++ },++ "capabilities": [++ "robot.say"++ ],++ "initial_state": {++ "wheel": {++ "entries": [++ "a"++ ]++ }++ }++}nnew file mode 100644@@@ -0,0 +1,27 @@++{++ "name": "Bad",++ "author": {++ "name": "Ada"++ },++ "sharing": "private",++ "look": {++ "component": "button",++ "label": "Bad"++ },++ "recipe": {++ "triggers": [++ {++ "kind": "press"++ }++ ],++ "actions": [++ {++ "kind": "robot.dance",++ "product": "bot"++ }++ ]++ },++ "capabilities": [++ "robot.say"++ ]++}nnew file mode 100644@@@ -0,0 +1,52 @@++{++ "name": "Headlight tally",++ "author": {++ "name": "Ada"++ },++ "sharing": "private",++ "look": {++ "component": "counter",++ "label": "Headlights",++ "style": {++ "tint": "surface",++ "shape": "square",++ "opacity": 0.8,++ "show_label": true++ }++ },++ "recipe": {++ "triggers": [++ {++ "kind": "chat_command",++ "command": "!headlights"++ }++ ],++ "conditions": {++ "role": "everyone",++ "cooldown": {++ "per_person_seconds": 30,++ "overall_seconds": 5++ }++ },++ "actions": [++ {++ "kind": "screen.counter",++ "product": "live",++ "key": "headlights",++ "by": 1++ },++ {++ "kind": "chat.message",++ "product": "chat",++ "text": "Headlights on!"++ }++ ]++ },++ "capabilities": [++ "screen.counter",++ "chat.message"++ ],++ "initial_state": {++ "headlights": 0++ }++}nnew file mode 100644@@@ -0,0 +1,75 @@++{++ "id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "revision": 3,++ "name": "Make it dance",++ "description": "The robot spins for a moment and confetti follows.",++ "author": {++ "name": "Maticus",++ "subject": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ }++ },++ "sharing": "gallery",++ "look": {++ "component": "button",++ "label": "Make it dance",++ "icon": "robot",++ "props": {++ "size": "large"++ },++ "style": {++ "tint": "accent",++ "shape": "pill",++ "opacity": 1,++ "show_label": true++ }++ },++ "recipe": {++ "triggers": [++ {++ "kind": "press"++ }++ ],++ "conditions": {++ "role": "follower",++ "cost": {++ "points": 50++ },++ "cooldown": {++ "per_person_seconds": 60,++ "overall_seconds": 10++ },++ "live_only": true,++ "daily_cap": {++ "per_person": 5++ },++ "chance": {++ "one_in": 1++ }++ },++ "actions": [++ {++ "kind": "robot.drive_pulse",++ "product": "bot",++ "direction": "spin_left",++ "ms": 800,++ "speed": 0.5++ },++ {++ "kind": "screen.confetti",++ "product": "live",++ "intensity": "medium"++ }++ ]++ },++ "capabilities": [++ "robot.drive_pulse",++ "screen.confetti"++ ],++ "initial_state": {++ "dances": 0++ },++ "created_at": "2026-11-11T20:00:00.000Z",++ "updated_at": "2026-11-11T20:30:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,40 @@++{++ "name": "Wheel of chatters",++ "author": {++ "name": "Ada"++ },++ "sharing": "link",++ "look": {++ "component": "wheel",++ "label": "Spin the wheel"++ },++ "recipe": {++ "triggers": [++ {++ "kind": "press"++ },++ {++ "kind": "chat_command",++ "command": "!wheel"++ }++ ],++ "actions": [++ {++ "kind": "robot.say",++ "product": "bot",++ "text": "The wheel says hello."++ }++ ]++ },++ "capabilities": [++ "robot.say"++ ],++ "mod": {++ "id": "mod_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "version": "1.2.0",++ "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",++ "capabilities": [++ "robot.say"++ ]++ }++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "catalog": {++ "id": "bot.controls",++ "version": 1,++ "name": "Robot controls",++ "devices": ["phone"],++ "grid": { "columns": { "phone": 4, "tablet": 8, "computer": 12 }, "cell_px": { "phone": 44, "tablet": 44, "computer": 44 } },++ "theme_tokens": ["danger"],++ "parts": [++ {++ "id": "stop", "name": "Stop", "kind": "button",++ "required": true, "pressed": true,++ "min": { "w": 2, "h": 2 },++ "styles": { "tints": ["danger"], "shapes": ["round"] },++ "bindings": { "keys": ["Space"], "gamepad": ["south"] }++ }++ ]++ },++ "layout": null,++ "words": "",++ "device": "phone"++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "catalog": {++ "id": "bot.controls",++ "version": 1,++ "name": "Robot controls",++ "devices": ["phone"],++ "grid": { "columns": { "phone": 4, "tablet": 8, "computer": 12 }, "cell_px": { "phone": 44, "tablet": 44, "computer": 44 } },++ "theme_tokens": ["danger"],++ "parts": [++ {++ "id": "stop", "name": "Stop", "kind": "button",++ "required": true, "pressed": true,++ "min": { "w": 2, "h": 2 },++ "styles": { "tints": ["danger"], "shapes": ["round"] },++ "bindings": { "keys": ["Space"], "gamepad": ["south"] }++ }++ ]++ },++ "layout": null,++ "words": "make it work with one thumb",++ "device": "phone"++}nnew file mode 100644@@@ -0,0 +1,40 @@++{++ "catalog": {++ "id": "bot.controls",++ "version": 1,++ "name": "Robot controls",++ "devices": ["phone"],++ "grid": { "columns": { "phone": 4, "tablet": 8, "computer": 12 }, "cell_px": { "phone": 44, "tablet": 44, "computer": 44 } },++ "theme_tokens": ["danger", "surface"],++ "parts": [++ {++ "id": "stop", "name": "Stop", "kind": "button",++ "required": true, "pressed": true,++ "min": { "w": 2, "h": 2, "px": 64 },++ "styles": { "tints": ["danger"], "shapes": ["round"] },++ "bindings": { "keys": ["Space"], "gamepad": ["south"] }++ },++ {++ "id": "stick", "name": "Drive stick", "kind": "stick",++ "required": false, "pressed": true,++ "min": { "w": 3, "h": 3 },++ "styles": { "tints": ["surface"], "shapes": ["round"] },++ "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] }++ }++ ]++ },++ "layout": {++ "surface": "bot.controls",++ "surface_version": 1,++ "device": "phone",++ "name": "Default",++ "author": { "name": "OpenVibe" },++ "sharing": "private",++ "parts": [++ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": ["Space"], "gamepad": ["south"] } },++ { "id": "stick", "visible": true, "place": { "x": 0, "y": 2, "w": 4, "h": 3 }, "style": { "tint": "surface", "shape": "round", "opacity": 1, "label": false }, "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] } }++ ]++ },++ "words": "put Stop on the left and make it huge, and give me a vertical head slider on the right edge",++ "device": "phone"++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "ok": false,++ "layout": {++ "surface": "bot.controls",++ "surface_version": 1,++ "device": "phone",++ "name": "Still here",++ "author": { "name": "Ana" },++ "sharing": "private",++ "parts": [++ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": [], "gamepad": [] } }++ ]++ },++ "summary": "Couldn't do that.",++ "reasons": ["Stop has to stay visible."]++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "ok": true,++ "layout": null,++ "summary": "Put Stop on the left.",++ "reasons": []++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "ok": false,++ "layout": null,++ "summary": "Couldn't do that: Stop has to stay visible.",++ "reasons": ["Stop has to stay visible.", "Every pressed part has to be at least 44 px."],++ "receipt": { "model": "deepseek-flash", "provider": "deepseek", "route": "ui.layout.suggest", "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPR" }++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "ok": true,++ "layout": {++ "surface": "bot.controls",++ "surface_version": 1,++ "device": "phone",++ "name": "One-thumb controls",++ "author": { "name": "Ana" },++ "sharing": "private",++ "parts": [++ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 4, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": ["Space"], "gamepad": ["south"] } },++ { "id": "stick", "visible": true, "place": { "x": 0, "y": 2, "w": 4, "h": 3 }, "style": { "tint": "surface", "shape": "round", "opacity": 1, "label": false }, "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] } }++ ]++ },++ "summary": "Put Stop across the top and the drive stick below it where a thumb reaches.",++ "reasons": ["Stop had to stay visible, so it became the top row.", "The stick moved under the thumb."],++ "receipt": { "model": "deepseek-flash", "provider": "deepseek", "route": "ui.layout.suggest", "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ", "cost_usd": 0.0004 }++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "surface": "bot.controls",++ "surface_version": 1,++ "device": "phone",++ "name": "Bad tint",++ "author": { "name": "Ana" },++ "sharing": "private",++ "parts": [++ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "#ff0000", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": [], "gamepad": [] } }++ ]++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "surface": "bot.controls",++ "surface_version": 1,++ "device": "phone",++ "name": "Too faint",++ "author": { "name": "Ana" },++ "sharing": "private",++ "parts": [++ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 0.2, "label": true }, "bindings": { "keys": [], "gamepad": [] } }++ ]++}nnew file mode 100644@@@ -0,0 +1,56 @@++{++ "surface": "bot.overlay",++ "surface_version": 1,++ "device": "computer",++ "name": "One-thumb overlay",++ "author": {++ "name": "Ana"++ },++ "sharing": "link",++ "parts": [++ {++ "id": "stop",++ "visible": true,++ "place": {++ "dock": "right",++ "order": 0,++ "w": 2,++ "h": 1++ },++ "style": {++ "tint": "danger",++ "shape": "round",++ "opacity": 1,++ "label": true++ },++ "bindings": {++ "keys": [++ "Space"++ ],++ "gamepad": [++ "south"++ ]++ }++ },++ {++ "id": "battery",++ "visible": true,++ "place": {++ "dock": "right",++ "order": 1,++ "w": 2,++ "h": 1++ },++ "style": {++ "tint": "surface",++ "shape": "pill",++ "opacity": 0.8,++ "label": false++ },++ "bindings": {++ "keys": [],++ "gamepad": []++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "surface": "bot.controls",++ "surface_version": 1,++ "device": "phone",++ "name": "My phone controls",++ "author": { "name": "Ana", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" } },++ "sharing": "private",++ "parts": [++ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": ["Space"], "gamepad": ["south"] } },++ { "id": "estop", "visible": true, "place": { "x": 2, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "square", "opacity": 1, "label": true }, "bindings": { "keys": ["Escape"], "gamepad": ["start"] } },++ { "id": "stick", "visible": true, "place": { "x": 0, "y": 2, "w": 4, "h": 3 }, "style": { "tint": "surface", "shape": "round", "opacity": 1, "label": false }, "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] } },++ { "id": "battery", "visible": true, "place": { "x": 0, "y": 5, "w": 2, "h": 1 }, "style": { "tint": "success", "shape": "pill", "opacity": 1, "label": true }, "bindings": { "keys": [], "gamepad": [] } },++ { "id": "latency", "visible": false, "place": { "x": 2, "y": 5, "w": 2, "h": 1 }, "style": { "tint": "muted", "shape": "pill", "opacity": 1, "label": true }, "bindings": { "keys": [], "gamepad": [] } }++ ],++ "updated_at": "2026-11-11T12:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "id": "bot.controls",++ "version": 1,++ "name": "Robot controls",++ "devices": ["phone"],++ "grid": { "columns": { "phone": 4, "tablet": 8, "computer": 12 }, "cell_px": { "phone": 44, "tablet": 44, "computer": 44 } },++ "theme_tokens": ["danger"],++ "parts": [++ {++ "id": "stop", "name": "Stop", "kind": "button",++ "required": true, "pressed": true,++ "min": { "w": 2, "h": 2 },++ "styles": { "tints": ["#ff0000"], "shapes": ["round"] },++ "bindings": { "keys": ["Space"], "gamepad": ["south"] }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "id": "bot.controls",++ "version": 1,++ "name": "Robot controls",++ "devices": ["phone"],++ "grid": { "columns": { "phone": 4, "tablet": 8, "computer": 12 }, "cell_px": { "phone": 44, "tablet": 44, "computer": 44 } },++ "theme_tokens": ["accent"],++ "parts": [++ {++ "id": "stop", "name": "Stop", "kind": "power",++ "required": true, "pressed": true,++ "min": { "w": 2, "h": 2 },++ "styles": { "tints": ["danger"], "shapes": ["round"] },++ "bindings": { "keys": ["Space"], "gamepad": ["south"] }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,64 @@++{++ "id": "bot.controls",++ "version": 1,++ "name": "Robot controls",++ "description": "The robot's own panel: Stop and the e-stop always visible, then the stick, the head slider and the readouts.",++ "devices": ["phone", "tablet", "computer"],++ "grid": {++ "columns": { "phone": 4, "tablet": 8, "computer": 12 },++ "cell_px": { "phone": 44, "tablet": 44, "computer": 44 }++ },++ "theme_tokens": ["accent", "surface", "surface-raised", "text", "muted", "danger", "warning", "success"],++ "parts": [++ {++ "id": "stop", "name": "Stop", "kind": "button",++ "required": true, "pressed": true,++ "min": { "w": 2, "h": 2, "px": 64 },++ "sizes": [{ "w": 2, "h": 2 }, { "w": 3, "h": 2 }],++ "styles": { "tints": ["danger", "warning"], "shapes": ["round", "pill", "square"], "opacity": { "min": 0.7, "max": 1 }, "label": true },++ "bindings": { "keys": ["Space", "Escape"], "gamepad": ["south", "start"] },++ "never_covers": []++ },++ {++ "id": "estop", "name": "E-stop", "kind": "button",++ "required": true, "pressed": true,++ "min": { "w": 2, "h": 2, "px": 64 },++ "styles": { "tints": ["danger"], "shapes": ["round", "square"], "opacity": { "min": 0.8, "max": 1 }, "label": true },++ "bindings": { "keys": ["Escape"], "gamepad": ["start"] },++ "never_covers": []++ },++ {++ "id": "stick", "name": "Drive stick", "kind": "stick",++ "required": false, "pressed": true,++ "min": { "w": 3, "h": 3 },++ "sizes": [{ "w": 3, "h": 3 }, { "w": 4, "h": 4 }],++ "styles": { "tints": ["surface", "surface-raised"], "shapes": ["round"], "opacity": { "min": 0.6, "max": 1 }, "label": false },++ "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] },++ "never_covers": ["stop", "estop"]++ },++ {++ "id": "head", "name": "Head", "kind": "slider",++ "required": false, "pressed": true,++ "min": { "w": 2, "h": 2 },++ "styles": { "tints": ["accent", "surface-raised"], "shapes": ["pill", "square"], "opacity": { "min": 0.4, "max": 1 }, "label": true },++ "bindings": { "keys": ["ArrowUp", "ArrowDown"], "gamepad": ["right-stick-y"] },++ "never_covers": ["stop", "estop"]++ },++ {++ "id": "battery", "name": "Battery", "kind": "readout",++ "required": false, "pressed": false,++ "min": { "w": 1, "h": 1 },++ "styles": { "tints": ["text", "muted", "success"], "shapes": ["pill", "square"], "opacity": { "min": 0.4, "max": 1 }, "label": true },++ "bindings": { "keys": [], "gamepad": [] },++ "never_covers": ["stop", "estop"]++ },++ {++ "id": "latency", "name": "Latency", "kind": "readout",++ "required": false, "pressed": false,++ "min": { "w": 1, "h": 1 },++ "styles": { "tints": ["text", "muted"], "shapes": ["pill", "square"], "opacity": { "min": 0.4, "max": 1 }, "label": true },++ "bindings": { "keys": [], "gamepad": [] },++ "never_covers": ["stop", "estop"]++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "kind": "chat_command",++ "command": "dance"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "kind": "hold",++ "ms": 10++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "kind": "cheer"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "kind": "chat_command",++ "command": "!dance"++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "kind": "press"++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "kind": "robot_telemetry",++ "metric": "battery_percent",++ "op": "lt",++ "value": 20,++ "for_seconds": 30++}@@@ -130805,6 +130805,3579 @@}}}++ },++ "ui.surface": {++ "version": "1.0.0",++ "owner": "contracts",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/surface.v1.json",++ "title": "UiSurface",++ "description": "ui.surface@1: the catalog a product publishes for one surface people may arrange (bot.controls, bot.overlay, live.channel, dock, chat.composer, navbar.shortcuts). It names the surface, its device classes and grid, the theme tokens its styles draw from, and every part a layout may place. A part says what it is (stick, button, slider, readout, video, list), whether it is required or pressed, its minimum cells and the minimum pixels the product guarantees (44 px for anything pressed, larger for Stop), the sizes and styles it allows, the bindings it accepts, and the parts it must never cover. A layout is validated against this catalog (openvibe-sdk/ui-layout); a layout can arrange only what the catalog has and can never add a power. The catalog is data: a layout or an AI answer never carries code or a URL.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "version",++ "name",++ "devices",++ "grid",++ "theme_tokens",++ "parts"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",++ "maxLength": 80,++ "description": "The surface id: <product>.<surface>, e.g. bot.controls."++ },++ "version": {++ "type": "integer",++ "minimum": 1,++ "description": "The catalog version. A layout names the version it was made against."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "What the surface is called where a person sees it."++ },++ "description": {++ "type": "string",++ "maxLength": 500++ },++ "devices": {++ "type": "array",++ "minItems": 1,++ "maxItems": 3,++ "uniqueItems": true,++ "items": {++ "enum": [++ "phone",++ "tablet",++ "computer"++ ]++ },++ "description": "The device classes this surface offers."++ },++ "grid": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "columns",++ "cell_px"++ ],++ "properties": {++ "columns": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "phone",++ "tablet",++ "computer"++ ],++ "description": "The grid's columns per device class: 4 on a phone and 12 on a computer in the design; a product may choose otherwise within these bounds.",++ "properties": {++ "phone": {++ "type": "integer",++ "minimum": 2,++ "maximum": 8++ },++ "tablet": {++ "type": "integer",++ "minimum": 2,++ "maximum": 16++ },++ "computer": {++ "type": "integer",++ "minimum": 2,++ "maximum": 24++ }++ }++ },++ "cell_px": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "phone",++ "tablet",++ "computer"++ ],++ "description": "The smallest a cell is rendered at that device class, in CSS pixels, at the narrowest supported width. The validator multiplies a part's cells by this to check the 44 px rule.",++ "properties": {++ "phone": {++ "type": "integer",++ "minimum": 24,++ "maximum": 200++ },++ "tablet": {++ "type": "integer",++ "minimum": 24,++ "maximum": 200++ },++ "computer": {++ "type": "integer",++ "minimum": 24,++ "maximum": 200++ }++ }++ }++ }++ },++ "theme_tokens": {++ "type": "array",++ "minItems": 1,++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,39}$"++ },++ "description": "The theme tokens a tint may name (accent, surface, danger, ...). Never a colour value: the theme decides what the token is."++ },++ "parts": {++ "type": "array",++ "minItems": 1,++ "maxItems": 64,++ "items": {++ "$ref": "#/$defs/part"++ }++ }++ },++ "$defs": {++ "part": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "name",++ "kind",++ "required",++ "pressed",++ "min",++ "styles",++ "bindings"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$",++ "description": "Stable part id inside this catalog, e.g. stop, stick, battery."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "kind": {++ "enum": [++ "stick",++ "button",++ "slider",++ "readout",++ "video",++ "list"++ ]++ },++ "required": {++ "type": "boolean",++ "description": "A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused."++ },++ "pressed": {++ "type": "boolean",++ "description": "The part sends a command when pressed or moved; it must render at least 44 px on every side."++ },++ "min": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "w",++ "h"++ ],++ "description": "The smallest size a layout may give the part: w and h in grid cells, and px when the product guarantees more than 44 px (Stop).",++ "properties": {++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "px": {++ "type": "integer",++ "minimum": 1,++ "maximum": 400++ }++ }++ },++ "sizes": {++ "type": "array",++ "maxItems": 12,++ "uniqueItems": true,++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "w",++ "h"++ ],++ "properties": {++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ }++ }++ },++ "description": "Sizes the editor may offer as presets. A layout is not limited to these: it is limited to min and to the grid."++ },++ "styles": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "tints",++ "shapes"++ ],++ "properties": {++ "tints": {++ "type": "array",++ "minItems": 1,++ "maxItems": 16,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,39}$"++ },++ "description": "The theme tokens this part may be tinted with; every one is also in the surface's theme_tokens."++ },++ "shapes": {++ "type": "array",++ "minItems": 1,++ "maxItems": 3,++ "uniqueItems": true,++ "items": {++ "enum": [++ "round",++ "pill",++ "square"++ ]++ }++ },++ "opacity": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "min",++ "max"++ ],++ "properties": {++ "min": {++ "type": "number",++ "minimum": 0.4,++ "maximum": 1++ },++ "max": {++ "type": "number",++ "minimum": 0.4,++ "maximum": 1++ }++ },++ "description": "The opacity range this part allows; the design's range is 0.4 to 1."++ },++ "label": {++ "type": "boolean",++ "description": "Whether the editor may show the part's label on it."++ }++ }++ },++ "bindings": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "keys",++ "gamepad"++ ],++ "properties": {++ "keys": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"++ }++ },++ "gamepad": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ }++ }++ },++ "description": "The bindings the product accepts for this part. A layout may pick a subset, never add one."++ },++ "never_covers": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ },++ "description": "The parts this part must never cover, in addition to the surface-wide rule that nothing covers a required part."++ }++ }++ }++ }++ }++ },++ "ui.layout": {++ "version": "1.0.0",++ "owner": "contracts",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/layout.v1.json",++ "title": "UiLayout",++ "description": "ui.layout@1: one person's arrangement of one surface on one device class. A layout names the surface and its catalog version, the device, its own name, its author and its sharing (private, link or gallery), and one entry per part it places. A part is visible or not, sits on the surface's grid (x, y, w, h in cells) or docks to an edge for an overlay, and carries a style (a theme token tint, a shape, opacity 0.4-1 and the label) and bindings. It is data only: no URL, no markup and no code may appear anywhere in it, and every part id, tint, shape, opacity, size and binding must be one the catalog allows. openvibe-sdk/ui-layout validates it before any server, editor or AI answer stores or shows it.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "surface",++ "surface_version",++ "device",++ "name",++ "author",++ "sharing",++ "parts"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Assigned by OpenVibe.Network when the layout is stored; absent in a draft, a share answer or an AI suggestion."++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "description": "Assigned by the store; every change to a stored layout moves it by one."++ },++ "active": {++ "type": "boolean",++ "description": "Set by OpenVibe.Network: the one layout a product draws for this surface on this device class. A draft, a share answer and an AI suggestion carry no active field."++ },++ "surface": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",++ "maxLength": 80++ },++ "surface_version": {++ "type": "integer",++ "minimum": 1++ },++ "device": {++ "enum": [++ "phone",++ "tablet",++ "computer"++ ]++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "The person's name for this layout; \"My phone controls\" is fine."++ },++ "author": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "name"++ ],++ "properties": {++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "subject": {++ "$ref": "../identity/subject-ref.v1.json"++ }++ },++ "description": "The author's display name and, for a private or link layout, the subject. A share or gallery answer carries the display name only, never the account."++ },++ "sharing": {++ "enum": [++ "private",++ "link",++ "gallery"++ ]++ },++ "parts": {++ "type": "array",++ "minItems": 1,++ "maxItems": 64,++ "items": {++ "$ref": "#/$defs/part"++ }++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "$defs": {++ "place": {++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "x",++ "y",++ "w",++ "h"++ ],++ "properties": {++ "x": {++ "type": "integer",++ "minimum": 0,++ "maximum": 23++ },++ "y": {++ "type": "integer",++ "minimum": 0,++ "maximum": 100++ },++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ }++ },++ "description": "The part's cell on the surface grid."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "dock",++ "order",++ "w",++ "h"++ ],++ "properties": {++ "dock": {++ "enum": [++ "left",++ "right",++ "top",++ "bottom"++ ]++ },++ "order": {++ "type": "integer",++ "minimum": 0,++ "maximum": 63++ },++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ }++ },++ "description": "For an overlay: the edge the part docks to, its order along that edge, and its size in cells."++ }++ ]++ },++ "style": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "tint",++ "shape",++ "opacity",++ "label"++ ],++ "properties": {++ "tint": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,39}$",++ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."++ },++ "shape": {++ "enum": [++ "round",++ "pill",++ "square"++ ]++ },++ "opacity": {++ "type": "number",++ "minimum": 0.4,++ "maximum": 1++ },++ "label": {++ "type": "boolean"++ }++ }++ },++ "bindings": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "keys",++ "gamepad"++ ],++ "properties": {++ "keys": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"++ }++ },++ "gamepad": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ }++ }++ }++ },++ "part": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "visible",++ "place",++ "style",++ "bindings"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ },++ "visible": {++ "type": "boolean"++ },++ "place": {++ "$ref": "#/$defs/place"++ },++ "style": {++ "$ref": "#/$defs/style"++ },++ "bindings": {++ "$ref": "#/$defs/bindings"++ }++ }++ }++ }++ }++ },++ "ui.layout-request": {++ "version": "1.0.0",++ "owner": "contracts",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/layout-request.v1.json",++ "title": "UiLayoutRequest",++ "description": "ui.layout-request@1: what an editor sends OpenVibe.AI's ui.layout.suggest task, and what Network forwards for a signed-in person. It is the surface's catalog, the layout shown now (or null for the default one) and the words the person typed (max 500 characters), for one device class. The catalog is the only power the answer can use; the current layout is the starting arrangement; the words never contain markup that becomes part of it.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "catalog",++ "layout",++ "words",++ "device"++ ],++ "properties": {++ "catalog": {++ "$ref": "surface.v1.json"++ },++ "layout": {++ "oneOf": [++ {++ "$ref": "layout.v1.json"++ },++ {++ "type": "null"++ }++ ],++ "description": "The arrangement shown now, or null when the person is looking at the default."++ },++ "words": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500,++ "description": "The person's own description, e.g. \"put Stop on the left and make it huge\"."++ },++ "device": {++ "enum": [++ "phone",++ "tablet",++ "computer"++ ]++ }++ }++ }++ },++ "ui.layout-suggestion": {++ "version": "1.0.0",++ "owner": "contracts",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/layout-suggestion.v1.json",++ "title": "UiLayoutSuggestion",++ "description": "ui.layout-suggestion@1: OpenVibe.AI's answer to ui.layout.suggest. ok true carries the layout, already checked by openvibe-sdk/ui-layout against the catalog sent with the request, and a one-sentence summary of what changed plus the model's reasons. ok false is an honest refusal: no layout, one sentence saying what it could not do, and the validator's reasons in plain words (for example \"Stop has to stay visible.\"). A receipt names the model that answered, with the run and the route. The suggestion is previewed, never applied silently; Network or the product stores it only after the person accepts it.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "ok",++ "layout",++ "summary",++ "reasons"++ ],++ "properties": {++ "ok": {++ "type": "boolean"++ },++ "layout": {++ "oneOf": [++ {++ "$ref": "layout.v1.json"++ },++ {++ "type": "null"++ }++ ],++ "description": "The validated layout when ok is true; null when it is false."++ },++ "summary": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300,++ "description": "One sentence saying what changed, or what could not be done."++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300++ },++ "description": "Why this arrangement, or the validator's reasons for refusing one."++ },++ "receipt": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "model"++ ],++ "properties": {++ "model": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "provider": {++ "type": "string",++ "minLength": 1,++ "maxLength": 100++ },++ "route": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "run_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "cost_usd": {++ "type": "number",++ "minimum": 0++ }++ },++ "description": "Which model answered: the same receipt every AI run carries, at the task's own shape."++ }++ },++ "allOf": [++ {++ "if": {++ "properties": {++ "ok": {++ "const": true++ }++ },++ "required": [++ "ok"++ ]++ },++ "then": {++ "properties": {++ "layout": {++ "type": "object"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "ok": {++ "const": false++ }++ },++ "required": [++ "ok"++ ]++ },++ "then": {++ "properties": {++ "layout": {++ "type": "null"++ }++ }++ }++ }++ ]++ }++ },++ "network.layout.changed": {++ "version": "1.0.0",++ "owner": "network",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/network.layout.changed.v1.json",++ "title": "NetworkLayoutChangedPayload",++ "description": "network.layout.changed v1 (OpenVibe.Network server/layouts, written into network_event_outbox in the transaction that changes a stored layout). One event per change to one person's ui.layout@1: set active, saved, renamed, deleted, shared, unshared, published to the gallery, unpublished, copied from the gallery, or reset to the default. Envelope: subject { type: user, id } and the layout's revision, actor the person, visibility internal. The payload carries no account data and no layout contents, so an open page can announce a change and refetch with the person's own token.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "surface",++ "device",++ "layout_id",++ "change",++ "revision"++ ],++ "properties": {++ "surface": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",++ "maxLength": 80++ },++ "device": {++ "enum": [++ "phone",++ "tablet",++ "computer"++ ]++ },++ "layout_id": {++ "type": "string",++ "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "change": {++ "enum": [++ "active",++ "saved",++ "renamed",++ "deleted",++ "shared",++ "unshared",++ "published",++ "unpublished",++ "copied",++ "reset"++ ]++ },++ "revision": {++ "type": "integer",++ "minimum": 1++ },++ "sharing": {++ "enum": [++ "private",++ "link",++ "gallery"++ ]++ }++ }++ }++ },++ "ui.action": {++ "version": "1.0.0",++ "owner": "contracts",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/action.v1.json",++ "title": "UiAction",++ "description": "ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop, the e-stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.",++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "direction",++ "ms"++ ],++ "description": "Drive the robot for a moment, then stop. The duration is bounded by Bot's own max_command_ms and the owner's limits for the presser's role.",++ "properties": {++ "kind": {++ "const": "robot.drive_pulse"++ },++ "product": {++ "const": "bot"++ },++ "direction": {++ "enum": [++ "forward",++ "back",++ "left",++ "right",++ "spin_left",++ "spin_right"++ ]++ },++ "ms": {++ "type": "integer",++ "minimum": 50,++ "maximum": 3000++ },++ "speed": {++ "type": "number",++ "minimum": 0.1,++ "maximum": 1,++ "description": "Fraction of the robot's allowed speed; omitted means the owner's default for the role."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "preset"++ ],++ "description": "Take the robot's head to a preset. Bot maps up, center and down to the head actuator its robot profile declares; a robot without one refuses it.",++ "properties": {++ "kind": {++ "const": "robot.head_preset"++ },++ "product": {++ "const": "bot"++ },++ "preset": {++ "enum": [++ "up",++ "center",++ "down"++ ],++ "description": "A head tilt preset. Bot still checks the robot profile and the owner's limits."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "on"++ ],++ "description": "Turn the robot's headlight on or off. Bot's own headlight kind is the authority: only the owner and People Operators may send it, and a robot whose profile has no headlight refuses it.",++ "properties": {++ "kind": {++ "const": "robot.headlight"++ },++ "product": {++ "const": "bot"++ },++ "on": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "text"++ ],++ "description": "Speak a line through the robot. Text goes through the existing content checks and Bot's own speak limits.",++ "properties": {++ "kind": {++ "const": "robot.say"++ },++ "product": {++ "const": "bot"++ },++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "macro"++ ],++ "description": "Replay a recorded robot macro through the safe command path, with its own deadline. Macros are stage 4 of Elements; a stage-1 product refuses this kind as not yet supported.",++ "properties": {++ "kind": {++ "const": "robot.macro"++ },++ "product": {++ "const": "bot"++ },++ "macro": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "text"++ ],++ "description": "Send a chat message as the channel's bot. Text goes through Chat's content checks and rate limits.",++ "properties": {++ "kind": {++ "const": "chat.message"++ },++ "product": {++ "const": "chat"++ },++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 400++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "source"++ ],++ "properties": {++ "kind": {++ "const": "chat.pin"++ },++ "product": {++ "const": "chat"++ },++ "source": {++ "enum": [++ "trigger",++ "latest"++ ],++ "description": "trigger pins the message that caused this action; latest pins the newest chat message."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "question",++ "options"++ ],++ "properties": {++ "kind": {++ "const": "chat.poll"++ },++ "product": {++ "const": "chat"++ },++ "question": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "options": {++ "type": "array",++ "minItems": 2,++ "maxItems": 10,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ }++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "title"++ ],++ "properties": {++ "kind": {++ "const": "screen.alert"++ },++ "product": {++ "const": "live"++ },++ "title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "name"++ ],++ "properties": {++ "kind": {++ "const": "screen.animation"++ },++ "product": {++ "const": "live"++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "An animation the page or overlay has; an unknown name is refused, never fetched."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product"++ ],++ "properties": {++ "kind": {++ "const": "screen.confetti"++ },++ "product": {++ "const": "live"++ },++ "intensity": {++ "enum": [++ "low",++ "medium",++ "high"++ ]++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "sound"++ ],++ "properties": {++ "kind": {++ "const": "screen.sound"++ },++ "product": {++ "const": "live"++ },++ "sound": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "A sound from the streamer's own sound list; Live never fetches a URL for it."++ },++ "volume": {++ "type": "number",++ "minimum": 0,++ "maximum": 1++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "text",++ "voice"++ ],++ "properties": {++ "kind": {++ "const": "screen.tts"++ },++ "product": {++ "const": "live"++ },++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300++ },++ "voice": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "A voice from the streamer's own voice list."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "key",++ "by"++ ],++ "description": "Move one of the element's counters. The host applies it to the element's own state and Live broadcasts the new state.",++ "properties": {++ "kind": {++ "const": "screen.counter"++ },++ "product": {++ "const": "live"++ },++ "key": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{0,31}$"++ },++ "by": {++ "type": "integer",++ "minimum": -1000,++ "maximum": 1000++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "key",++ "value"++ ],++ "description": "Set one of the element's state values: a boolean, a number, a short string or a list of those (a wheel's entries, a scoreboard). Objects are not state values.",++ "properties": {++ "kind": {++ "const": "screen.state"++ },++ "product": {++ "const": "live"++ },++ "key": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{0,31}$"++ },++ "value": {++ "oneOf": [++ {++ "type": "boolean"++ },++ {++ "type": "number"++ },++ {++ "type": "string",++ "maxLength": 200++ },++ {++ "type": "array",++ "maxItems": 64,++ "items": {++ "oneOf": [++ {++ "type": "number"++ },++ {++ "type": "string",++ "maxLength": 200++ }++ ]++ }++ }++ ]++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "element",++ "visible"++ ],++ "properties": {++ "kind": {++ "const": "screen.visibility"++ },++ "product": {++ "const": "live"++ },++ "element": {++ "type": "string",++ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "visible": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "scene"++ ],++ "properties": {++ "kind": {++ "const": "screen.scene"++ },++ "product": {++ "const": "live"++ },++ "scene": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "A scene the layout has; an unknown scene is refused."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product"++ ],++ "properties": {++ "kind": {++ "const": "stream.clip"++ },++ "product": {++ "const": "live"++ },++ "seconds": {++ "type": "integer",++ "minimum": 5,++ "maximum": 30,++ "description": "How far back the clip starts, at most 30 seconds."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product"++ ],++ "properties": {++ "kind": {++ "const": "stream.moment"++ },++ "product": {++ "const": "live"++ },++ "label": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "title"++ ],++ "properties": {++ "kind": {++ "const": "stream.title"++ },++ "product": {++ "const": "live"++ },++ "title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 140++ }++ },++ "description": "Change the stream title. Live allows it only for the channel's owner, whatever the element's trigger says."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "category"++ ],++ "properties": {++ "kind": {++ "const": "stream.category"++ },++ "product": {++ "const": "live"++ },++ "category": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ }++ },++ "description": "Change the stream category. Live allows it only for the channel's owner."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "prompt"++ ],++ "description": "Ask an AI model through Exchange's catalogue, with the element's budget and a receipt. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",++ "properties": {++ "kind": {++ "const": "ai.ask"++ },++ "product": {++ "const": "ai"++ },++ "prompt": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500++ },++ "model": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "max_cost_usd": {++ "type": "number",++ "minimum": 0,++ "maximum": 1++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "task"++ ],++ "description": "Hand a task to an Actor agent. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",++ "properties": {++ "kind": {++ "const": "actor.task"++ },++ "product": {++ "const": "actor"++ },++ "task": {++ "type": "string",++ "minLength": 1,++ "maxLength": 8000++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "endpoint"++ ],++ "description": "Call one of the streamer's own webhook endpoints. The element names the endpoint the streamer listed; it never carries a URL, and the payload never carries a viewer's data. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",++ "properties": {++ "kind": {++ "const": "webhook.call"++ },++ "product": {++ "const": "live"++ },++ "endpoint": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_-]{0,31}$"++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "product",++ "goal"++ ],++ "description": "Update a tip goal's display. Money never moves here; Billing's own sheet confirms any charge. Stage 4 of Elements.",++ "properties": {++ "kind": {++ "const": "money.tip_goal"++ },++ "product": {++ "const": "billing"++ },++ "goal": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40++ }++ }++ }++ ],++ "$defs": {++ "capability": {++ "description": "The action kinds an Element may declare in ui.element@1 capabilities. One id, one product: the host routes an action only to the product that owns it.",++ "enum": [++ "robot.drive_pulse",++ "robot.head_preset",++ "robot.headlight",++ "robot.say",++ "robot.macro",++ "chat.message",++ "chat.pin",++ "chat.poll",++ "screen.alert",++ "screen.animation",++ "screen.confetti",++ "screen.sound",++ "screen.tts",++ "screen.counter",++ "screen.state",++ "screen.visibility",++ "screen.scene",++ "stream.clip",++ "stream.moment",++ "stream.title",++ "stream.category",++ "ai.ask",++ "actor.task",++ "webhook.call",++ "money.tip_goal"++ ]++ }++ }++ }++ },++ "ui.element": {++ "version": "1.0.0",++ "owner": "contracts",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/element.v1.json",++ "title": "UiElement",++ "description": "ui.element@1: an Element people make and place on a surface: a look, a behaviour and, only when a recipe is not enough, a mod. LOOK draws from OpenVibe.Design's components and the theme's tokens, so it follows the person's theme, the seasons and dark/light; it carries no markup and no URL. RECIPE is data only: one or more triggers from ui.trigger@1, the conditions that decide who may fire it and what it costs, and one or more actions from ui.action@1, checked against CAPABILITIES. A capability is an action kind the element declares; the host refuses an action the element did not declare, and the product named by the action still checks the presser's role, the cooldown, the cost and its own limits. MOD is an optional reference to a code module (id, version and digest) for the stage where a recipe is not enough; the module never adds a power. Conditions use channel points only in stage 1: no Vibes, no money, and Billing still confirms any charge itself. The id and revision are assigned by the store; a draft has neither.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "name",++ "author",++ "sharing",++ "look",++ "recipe",++ "capabilities"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Assigned by the store when the element is saved; absent in a draft."++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "description": "Assigned by the store; every saved change moves it by one."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "description": {++ "type": "string",++ "maxLength": 500++ },++ "author": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "name"++ ],++ "properties": {++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "subject": {++ "$ref": "../identity/subject-ref.v1.json"++ }++ },++ "description": "The author's display name and, for a private or link element, the subject. A gallery or copy answer carries the display name only, never the account."++ },++ "sharing": {++ "enum": [++ "private",++ "link",++ "gallery"++ ]++ },++ "look": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "component",++ "label"++ ],++ "properties": {++ "component": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,63}$",++ "description": "An OpenVibe.Design component name (design.component@1), e.g. button or meter. A name the page does not have is refused, never fetched."++ },++ "label": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "The words on the element, e.g. Make it dance."++ },++ "icon": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,31}$",++ "description": "A glyph name from the design icon set."++ },++ "props": {++ "type": "object",++ "maxProperties": 20,++ "propertyNames": {++ "pattern": "^[a-z][a-zA-Z0-9]{0,31}$"++ },++ "additionalProperties": {++ "oneOf": [++ {++ "type": "boolean"++ },++ {++ "type": "number"++ },++ {++ "type": "string",++ "maxLength": 200++ }++ ]++ },++ "description": "The design component's own props (the sliders the editor shows). Numbers, strings and booleans only; no nesting."++ },++ "style": {++ "$ref": "#/$defs/style"++ }++ }++ },++ "recipe": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "triggers",++ "actions"++ ],++ "properties": {++ "triggers": {++ "type": "array",++ "minItems": 1,++ "maxItems": 8,++ "items": {++ "$ref": "trigger.v1.json"++ }++ },++ "conditions": {++ "$ref": "#/$defs/conditions"++ },++ "actions": {++ "type": "array",++ "minItems": 1,++ "maxItems": 16,++ "items": {++ "$ref": "action.v1.json"++ }++ }++ }++ },++ "capabilities": {++ "type": "array",++ "minItems": 1,++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "$ref": "action.v1.json#/$defs/capability"++ },++ "description": "The action kinds this element may request. Every action in the recipe must be declared here; the host refuses the rest."++ },++ "initial_state": {++ "$ref": "element-state.v1.json#/$defs/values",++ "description": "The element's state before anything runs; the counter starts at 0, never at a value the element did not declare."++ },++ "mod": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "version",++ "sha256"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$"++ },++ "sha256": {++ "type": "string",++ "pattern": "^[0-9a-f]{64}$"++ },++ "capabilities": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "$ref": "action.v1.json#/$defs/capability"++ },++ "description": "The subset of the element's capabilities the module itself may request; omitted means the element's declared capabilities, and a product may grant fewer."++ }++ },++ "description": "A code module for an element whose recipe is not enough (stage 3). It runs sandboxed; the reference is data, and the host checks every request the module makes against the element's capabilities."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "$defs": {++ "style": {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "tint": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,39}$",++ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."++ },++ "shape": {++ "enum": [++ "round",++ "pill",++ "square"++ ]++ },++ "opacity": {++ "type": "number",++ "minimum": 0.4,++ "maximum": 1++ },++ "show_label": {++ "type": "boolean"++ }++ }++ },++ "conditions": {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "role": {++ "enum": [++ "everyone",++ "follower",++ "subscriber",++ "vip",++ "operator",++ "owner"++ ],++ "description": "Who may fire it. A higher role admits the lower ones where the product says so (an operator is admitted to subscriber-only); owner is the channel's owner alone. Default everyone."++ },++ "cost": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "points"++ ],++ "properties": {++ "points": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1000000++ }++ },++ "description": "Channel points the presser spends, from Live's points. Stage 1 has no other currency, and an element never charges money."++ },++ "cooldown": {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "per_person_seconds": {++ "type": "integer",++ "minimum": 0,++ "maximum": 86400++ },++ "overall_seconds": {++ "type": "integer",++ "minimum": 0,++ "maximum": 86400++ }++ },++ "description": "How long the same person must wait, and how long everybody must wait, before it can fire again."++ },++ "daily_cap": {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "per_person": {++ "type": "integer",++ "minimum": 1,++ "maximum": 10000++ },++ "overall": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1000000++ }++ },++ "description": "The most times it may fire in a UTC day, per person and for everyone."++ },++ "live_only": {++ "type": "boolean",++ "description": "When true the element fires only while the channel is live."++ },++ "robot_turn_only": {++ "type": "boolean",++ "description": "When true a robot action is requested only while the presser has the robot's turn; Bot still decides."++ },++ "chance": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "one_in"++ ],++ "properties": {++ "one_in": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1000000++ }++ },++ "description": "Fire only one time in N. The host draws the lot, so a refused draw costs nothing."++ }++ }++ }++ }++ }++ },++ "ui.element-state": {++ "version": "1.0.0",++ "owner": "contracts",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/element-state.v1.json",++ "title": "UiElementState",++ "description": "ui.element-state@1: the small store an Element keeps for itself (counters, scores, lists, a wheel's entries), as Live broadcasts it to everyone watching and as a read returns it. CHANNEL and ELEMENT_ID name the element instance, REVISION grows by one with each accepted change so a late frame never overwrites a newer one, and VALUES is the store itself: at most 32 keys, each a boolean, a number, a short string or a list of those. It is state, not code: no objects, no markup, no URL and no account data, which keeps a shared element's state safe to render for every viewer.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "channel",++ "element_id",++ "revision",++ "values",++ "updated_at"++ ],++ "properties": {++ "channel": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$",++ "description": "The Live channel's username (its slug)."++ },++ "element_id": {++ "type": "string",++ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 0++ },++ "values": {++ "$ref": "#/$defs/values"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "$defs": {++ "values": {++ "type": "object",++ "maxProperties": 32,++ "propertyNames": {++ "pattern": "^[a-z][a-z0-9_]{0,31}$"++ },++ "additionalProperties": {++ "$ref": "#/$defs/value"++ },++ "description": "The element's own store. A key is lower-case and 32 characters at most; a value is a boolean, a number, a string of 200 characters at most or a list of those."++ },++ "value": {++ "oneOf": [++ {++ "type": "boolean"++ },++ {++ "type": "number"++ },++ {++ "type": "string",++ "maxLength": 200++ },++ {++ "type": "array",++ "maxItems": 64,++ "items": {++ "oneOf": [++ {++ "type": "number"++ },++ {++ "type": "string",++ "maxLength": 200++ }++ ]++ }++ }++ ]++ }++ }++ }++ },++ "ui.trigger": {++ "version": "1.0.0",++ "owner": "contracts",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ui/trigger.v1.json",++ "title": "UiTrigger",++ "description": "ui.trigger@1: one trigger an Element's recipe listens for, from the fixed catalog. kind says what happened: a viewer pressing, holding or double-pressing the element, a key or gamepad binding, a chat command or text, a follow, a tip, an AI Moment, a Rundown topic, a timer or schedule, robot telemetry or an event, the stream starting or ending, a viewer-count crossing or another element's event. The fields a kind needs are the only fields it accepts. A trigger is data: it never names code or a URL, and a product may refuse a kind it does not offer yet. Stage 1 of Elements uses press and chat_command; the rest are declared here so a stage-2 element and the editor speak one language.",++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind"++ ],++ "properties": {++ "kind": {++ "const": "press"++ }++ },++ "description": "The element itself was pressed. This is the Viewer Deck's button."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "ms"++ ],++ "properties": {++ "kind": {++ "const": "hold"++ },++ "ms": {++ "type": "integer",++ "minimum": 300,++ "maximum": 10000++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind"++ ],++ "properties": {++ "kind": {++ "const": "double_press"++ },++ "within_ms": {++ "type": "integer",++ "minimum": 100,++ "maximum": 2000,++ "description": "The longest gap between the two presses; 400 by default."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "key"++ ],++ "properties": {++ "kind": {++ "const": "key"++ },++ "key": {++ "type": "string",++ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"++ },++ "modifiers": {++ "type": "array",++ "maxItems": 4,++ "uniqueItems": true,++ "items": {++ "enum": [++ "ctrl",++ "alt",++ "shift",++ "meta"++ ]++ }++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "button"++ ],++ "properties": {++ "kind": {++ "const": "gamepad"++ },++ "button": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ },++ "index": {++ "type": "integer",++ "minimum": 0,++ "maximum": 7,++ "description": "Which connected pad; omitted means any."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "command"++ ],++ "properties": {++ "kind": {++ "const": "chat_command"++ },++ "command": {++ "type": "string",++ "pattern": "^![a-z0-9_-]{1,32}$",++ "description": "The command without arguments, lower case, e.g. !dance."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "words"++ ],++ "properties": {++ "kind": {++ "const": "chat_text"++ },++ "words": {++ "type": "array",++ "minItems": 1,++ "maxItems": 16,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40++ }++ },++ "mode": {++ "enum": [++ "any",++ "all"++ ],++ "description": "any (default) matches one of the words; all matches every one."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind"++ ],++ "properties": {++ "kind": {++ "const": "follow"++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind"++ ],++ "properties": {++ "kind": {++ "const": "tip"++ },++ "min": {++ "type": "integer",++ "minimum": 0,++ "description": "The smallest tip, in Vibes, that fires the trigger; omitted means any tip."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind"++ ],++ "properties": {++ "kind": {++ "const": "ai_moment"++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind"++ ],++ "properties": {++ "kind": {++ "const": "rundown_topic"++ },++ "words": {++ "type": "array",++ "maxItems": 8,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40++ },++ "description": "Optional words the topic has to mention; omitted means any topic."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "every_seconds"++ ],++ "properties": {++ "kind": {++ "const": "timer"++ },++ "every_seconds": {++ "type": "integer",++ "minimum": 5,++ "maximum": 86400++ },++ "start": {++ "enum": [++ "live",++ "always"++ ],++ "description": "live (default) counts only while the channel is live; always counts from the element being active."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "cron",++ "timezone"++ ],++ "properties": {++ "kind": {++ "const": "schedule"++ },++ "cron": {++ "type": "string",++ "pattern": "^[0-9*/, -]{9,80}$",++ "description": "A five-field cron in the person's timezone."++ },++ "timezone": {++ "type": "string",++ "minLength": 1,++ "maxLength": 64++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "metric",++ "op",++ "value"++ ],++ "properties": {++ "kind": {++ "const": "robot_telemetry"++ },++ "metric": {++ "enum": [++ "battery_percent",++ "latency_ms",++ "stuck",++ "temperature_c"++ ]++ },++ "op": {++ "enum": [++ "lt",++ "gt"++ ]++ },++ "value": {++ "type": "number"++ },++ "for_seconds": {++ "type": "integer",++ "minimum": 0,++ "maximum": 3600,++ "description": "How long the condition must hold; 0 (default) means at once."++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "event"++ ],++ "properties": {++ "kind": {++ "const": "robot_event"++ },++ "event": {++ "enum": [++ "estop",++ "stopped",++ "disconnected",++ "command_refused",++ "button"++ ]++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind"++ ],++ "properties": {++ "kind": {++ "const": "stream_start"++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind"++ ],++ "properties": {++ "kind": {++ "const": "stream_end"++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "op",++ "count"++ ],++ "properties": {++ "kind": {++ "const": "viewer_count"++ },++ "op": {++ "enum": [++ "above",++ "below"++ ]++ },++ "count": {++ "type": "integer",++ "minimum": 0,++ "maximum": 100000000++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "element_id"++ ],++ "properties": {++ "kind": {++ "const": "element_event"++ },++ "element_id": {++ "type": "string",++ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "event": {++ "enum": [++ "pressed",++ "state",++ "shown"++ ],++ "description": "pressed (default) fires after that element ran; state and shown fire when Live broadcasts them."++ }++ }++ }++ ]++ }++ },++ "live.element.pressed": {++ "version": "1.0.0",++ "owner": "live",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/live.element.pressed.v1.json",++ "title": "LiveElementPressedPayload",++ "description": "live.element.pressed v1 (OpenVibe.Live server/elements): a viewer's element press was accepted and ran. It carries the public facts the stream should see: the channel, the element's id and name, the presser's public name and subject, the trigger that fired, the channel points it cost and the element-state revision it left behind. It never carries the presser's email, address, account balance or anything private; a refused press does not produce it. Envelope: subject { type: element, id: el_… }, actor the presser's subject, visibility public, priority low. Also written to the channel's AI timeline as a moment, so the stream's own timeline can cite 'chat made the robot dance'.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "channel",++ "element_id",++ "element_name",++ "presser",++ "presser_name",++ "trigger",++ "at"++ ],++ "properties": {++ "channel": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "username",++ "display_name"++ ],++ "properties": {++ "username": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$"++ },++ "display_name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ }++ },++ "description": "The public channel facts (the channel page is public already)."++ },++ "element_id": {++ "type": "string",++ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "element_name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "presser": {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ "presser_name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120,++ "description": "The display name the stream shows, never an email or an account id."++ },++ "trigger": {++ "$ref": "../../ui/trigger.v1.json"++ },++ "points_spent": {++ "type": "integer",++ "minimum": 0,++ "description": "Channel points the press cost; 0 when it was free."++ },++ "state_revision": {++ "type": "integer",++ "minimum": 0,++ "description": "The element-state revision after the actions ran; 0 when the element has no state yet."++ },++ "at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ },++ "live.element.state": {++ "version": "1.0.0",++ "owner": "live",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/live.element.state.v1.json",++ "title": "LiveElementStatePayload",++ "description": "live.element.state v1 (OpenVibe.Live server/elements): an element's own store changed, broadcast to everyone watching through Live's existing events path. Carries the channel's public facts and the same store as ui.element-state@1: the element instance, a revision that grows by one per accepted change, the values themselves and when. It is public state (a counter, a score, a wheel's entries), never the presser's data: a value may not be an object, a markup string or a URL, and the payload carries no account.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "channel",++ "element_id",++ "revision",++ "values",++ "updated_at"++ ],++ "properties": {++ "channel": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "username",++ "display_name"++ ],++ "properties": {++ "username": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$"++ },++ "display_name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ }++ }++ },++ "element_id": {++ "type": "string",++ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 0++ },++ "values": {++ "$ref": "../../ui/element-state.v1.json#/$defs/values"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ },++ "ai.deep-dive": {++ "version": "1.0.0",++ "owner": "ai",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/ai/deep-dive.v1.json",++ "title": "AiDeepDive",++ "description": "A deep dive: expert design research run by OpenVibe.AI (workflow design.deep_dive) for OpenVibe.Exchange's /deep-dive page and, through the API, OpenVibe.Design. One document holds the question and depth (the input), the seven steps, every source read, every capture (a private Media object per viewport and theme, with its DOM facts and Expert extras), the per-site analysis, the synthesis across sites, the recommendations checked against OpenVibe's own rules, and the receipt of every model call and capture. Every claim carries its evidence: the site, the capture or crop, the URL and the date it was read. Nothing is copied: captures are private to the run's owner and the report shows them as attributed commentary.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "schema",++ "run_id",++ "status",++ "input",++ "steps",++ "sources",++ "captures",++ "analysis",++ "synthesis",++ "recommendations",++ "receipt",++ "gaps",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The run id; a deep dive is a run (GET /api/v1/deep-dives/{id})."++ },++ "schema": {++ "const": "ai.deep-dive@1"++ },++ "run_id": {++ "type": "string",++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "status": {++ "enum": [++ "queued",++ "running",++ "choosing",++ "succeeded",++ "failed",++ "cancelled"++ ]++ },++ "input": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "question",++ "depth",++ "max_sites"++ ],++ "properties": {++ "question": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500++ },++ "depth": {++ "enum": [++ "quick",++ "thorough",++ "expert"++ ]++ },++ "max_sites": {++ "type": "integer",++ "minimum": 1,++ "maximum": 30++ },++ "sites": {++ "type": "array",++ "maxItems": 30,++ "items": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "description": "Sites the person named when starting the run."++ }++ }++ },++ "steps": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "key",++ "status"++ ],++ "properties": {++ "key": {++ "enum": [++ "plan",++ "search",++ "choose",++ "capture",++ "analyse",++ "synthesise",++ "report"++ ]++ },++ "status": {++ "enum": [++ "pending",++ "running",++ "done",++ "skipped",++ "failed"++ ]++ },++ "detail": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "finished_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++ }++ },++ "sources": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "kind",++ "title",++ "url",++ "read_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "kind": {++ "enum": [++ "web",++ "picture",++ "icon",++ "project",++ "rules",++ "component",++ "named"++ ]++ },++ "title": {++ "type": "string",++ "maxLength": 400++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "publisher": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200++ },++ "engine": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 80++ },++ "snippet": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "license": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 120++ },++ "read_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ },++ "captures": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "source_id",++ "url",++ "captured_at",++ "viewport",++ "theme",++ "full_page",++ "crops"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^cap_[A-Za-z0-9_-]{1,40}$"++ },++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "final_url": {++ "type": [++ "string",++ "null"++ ],++ "format": "uri",++ "pattern": "^https?://"++ },++ "title": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 400++ },++ "captured_at": {++ "type": "string",++ "format": "date-time"++ },++ "viewport": {++ "enum": [++ 390,++ 1280++ ]++ },++ "theme": {++ "enum": [++ "light",++ "dark"++ ]++ },++ "full_page": {++ "type": "boolean"++ },++ "media_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "width": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "height": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "crops": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "media_id",++ "y",++ "height"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^crop_[A-Za-z0-9_-]{1,40}$"++ },++ "media_id": {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "y": {++ "type": "integer",++ "minimum": 0++ },++ "height": {++ "type": "integer",++ "minimum": 1++ }++ }++ }++ },++ "dom": {++ "type": [++ "object",++ "null"++ ]++ },++ "extras": {++ "type": [++ "object",++ "null"++ ]++ },++ "skip": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200,++ "description": "Why the capture did not happen: robots_disallowed, not_public, no_chrome, timeout, …"++ },++ "cached": {++ "type": "boolean"++ }++ }++ }++ },++ "analysis": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "source_id",++ "url",++ "summary",++ "strengths",++ "weaknesses",++ "evidence"++ ],++ "properties": {++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "summary": {++ "type": "string",++ "maxLength": 4000++ },++ "first_impression": {++ "type": "string",++ "maxLength": 2000++ },++ "hierarchy": {++ "type": "string",++ "maxLength": 2000++ },++ "density": {++ "type": "string",++ "maxLength": 2000++ },++ "type": {++ "type": "string",++ "maxLength": 2000++ },++ "colour": {++ "type": "string",++ "maxLength": 2000++ },++ "motion": {++ "type": "string",++ "maxLength": 2000++ },++ "words": {++ "type": "string",++ "maxLength": 2000++ },++ "strengths": {++ "type": "array",++ "maxItems": 12,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ },++ "weaknesses": {++ "type": "array",++ "maxItems": 12,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ },++ "accessibility": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "speed": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "evidence": {++ "$ref": "#/$defs/evidence"++ }++ }++ }++ },++ "synthesis": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "patterns",++ "comparison",++ "best",++ "mistakes",++ "gap",++ "reference_tokens",++ "do",++ "dont",++ "accessibility",++ "speed"++ ],++ "properties": {++ "patterns": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "name",++ "what",++ "sites",++ "evidence"++ ],++ "properties": {++ "name": {++ "type": "string",++ "maxLength": 160++ },++ "what": {++ "type": "string",++ "maxLength": 1200++ },++ "sites": {++ "type": "array",++ "maxItems": 30,++ "items": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ }++ },++ "usage_count": {++ "type": "integer",++ "minimum": 0,++ "maximum": 30++ },++ "evidence": {++ "$ref": "#/$defs/evidence"++ }++ }++ }++ },++ "comparison": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "columns",++ "rows"++ ],++ "properties": {++ "columns": {++ "type": "array",++ "maxItems": 12,++ "items": {++ "type": "string",++ "maxLength": 120++ }++ },++ "rows": {++ "type": "array",++ "items": {++ "type": "array",++ "maxItems": 12,++ "items": {++ "type": "string",++ "maxLength": 1000++ }++ }++ }++ }++ },++ "best": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "source_id",++ "what",++ "evidence"++ ],++ "properties": {++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "what": {++ "type": "string",++ "maxLength": 2000++ },++ "evidence": {++ "$ref": "#/$defs/evidence"++ }++ }++ },++ "mistakes": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "what",++ "evidence"++ ],++ "properties": {++ "what": {++ "type": "string",++ "maxLength": 1200++ },++ "evidence": {++ "$ref": "#/$defs/evidence"++ }++ }++ }++ },++ "gap": {++ "type": "string",++ "maxLength": 2000++ },++ "reference_tokens": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "palettes",++ "type_scales",++ "spacing"++ ],++ "properties": {++ "palettes": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "source_id",++ "colours"++ ],++ "properties": {++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "colours": {++ "type": "array",++ "maxItems": 16,++ "items": {++ "type": "string",++ "maxLength": 40++ }++ }++ }++ }++ },++ "type_scales": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "source_id",++ "families",++ "sizes"++ ],++ "properties": {++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "families": {++ "type": "array",++ "maxItems": 8,++ "items": {++ "type": "string",++ "maxLength": 120++ }++ },++ "sizes": {++ "type": "array",++ "maxItems": 16,++ "items": {++ "type": "string",++ "maxLength": 40++ }++ }++ }++ }++ },++ "spacing": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "source_id",++ "steps"++ ],++ "properties": {++ "source_id": {++ "type": "string",++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "steps": {++ "type": "array",++ "maxItems": 16,++ "items": {++ "type": "string",++ "maxLength": 40++ }++ }++ }++ }++ }++ }++ },++ "do": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ },++ "dont": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ },++ "accessibility": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ },++ "speed": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "maxLength": 600++ }++ }++ }++ },++ "recommendations": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "what",++ "why",++ "rule",++ "evidence"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^rec_[A-Za-z0-9_-]{1,40}$"++ },++ "what": {++ "type": "string",++ "maxLength": 1200++ },++ "why": {++ "type": "string",++ "maxLength": 2000++ },++ "rule": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "status",++ "text"++ ],++ "properties": {++ "id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 120++ },++ "status": {++ "enum": [++ "follows",++ "would_change",++ "none"++ ]++ },++ "text": {++ "type": "string",++ "maxLength": 600++ }++ }++ },++ "evidence": {++ "$ref": "#/$defs/evidence"++ }++ }++ }++ },++ "receipt": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "price_usd",++ "held_usd",++ "cost_usd",++ "models",++ "captures_count",++ "synthetic"++ ],++ "properties": {++ "price_usd": {++ "type": "number",++ "minimum": 0++ },++ "held_usd": {++ "type": "number",++ "minimum": 0++ },++ "cost_usd": {++ "type": "number",++ "minimum": 0++ },++ "captures_count": {++ "type": "integer",++ "minimum": 0++ },++ "synthetic": {++ "type": "boolean"++ },++ "models": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "step",++ "provider",++ "model",++ "tokens_in",++ "tokens_out",++ "cost_usd",++ "ms"++ ],++ "properties": {++ "step": {++ "enum": [++ "plan",++ "choose",++ "analyse",++ "synthesise"++ ]++ },++ "provider": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 80++ },++ "model": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200++ },++ "tokens_in": {++ "type": "integer",++ "minimum": 0++ },++ "tokens_out": {++ "type": "integer",++ "minimum": 0++ },++ "cost_usd": {++ "type": "number",++ "minimum": 0++ },++ "ms": {++ "type": "integer",++ "minimum": 0++ }++ }++ }++ }++ }++ },++ "gaps": {++ "type": "array",++ "maxItems": 40,++ "items": {++ "type": "string",++ "maxLength": 400++ }++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ },++ "finished_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ },++ "$defs": {++ "evidence": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "claim",++ "url",++ "read_at"++ ],++ "properties": {++ "claim": {++ "type": "string",++ "maxLength": 600++ },++ "source_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"++ },++ "capture_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^cap_[A-Za-z0-9_-]{1,40}$"++ },++ "crop_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^crop_[A-Za-z0-9_-]{1,40}$"++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "read_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ }++ }++ }++ },++ "ai.deep_dive.updated": {++ "version": "1.0.0",++ "owner": "ai",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/ai.deep_dive.updated.v1.json",++ "title": "AiDeepDiveUpdatedPayload",++ "description": "ai.deep_dive.updated v1 (OpenVibe.AI server/deep-dive): a deep dive's step changed or it ended. One event per step transition and one at the end, so a page (OpenVibe.Exchange's /deep-dive) can follow a run without polling, and OpenVibe.Design can attach a finished report. The payload carries the run and deep-dive ids, the step, the progress and the status; never the question's answer, a capture or a prompt. Envelope: subject { type: run, id: <run_id> }, visibility internal, priority low (important on failed), actor service:ai.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "deep_dive_id",++ "run_id",++ "status",++ "step",++ "progress",++ "steps",++ "depth",++ "requester",++ "on_behalf_of",++ "url",++ "error",++ "occurred_at"++ ],++ "properties": {++ "deep_dive_id": {++ "type": "string",++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "run_id": {++ "type": "string",++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "status": {++ "enum": [++ "queued",++ "running",++ "choosing",++ "succeeded",++ "failed",++ "cancelled"++ ]++ },++ "step": {++ "enum": [++ "plan",++ "search",++ "choose",++ "capture",++ "analyse",++ "synthesise",++ "report",++ "done",++ null++ ]++ },++ "progress": {++ "type": "number",++ "minimum": 0,++ "maximum": 1++ },++ "steps": {++ "type": "array",++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "key",++ "status"++ ],++ "properties": {++ "key": {++ "enum": [++ "plan",++ "search",++ "choose",++ "capture",++ "analyse",++ "synthesise",++ "report"++ ]++ },++ "status": {++ "enum": [++ "pending",++ "running",++ "done",++ "skipped",++ "failed"++ ]++ }++ }++ }++ },++ "depth": {++ "enum": [++ "quick",++ "thorough",++ "expert"++ ]++ },++ "requester": {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ "on_behalf_of": {++ "anyOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ]++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://"++ },++ "error": {++ "anyOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "code",++ "detail"++ ],++ "properties": {++ "code": {++ "type": "string",++ "maxLength": 80++ },++ "detail": {++ "type": "string",++ "maxLength": 500++ }++ }++ },++ {++ "type": "null"++ }++ ]++ },++ "occurred_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }}}}@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Actor API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.AI API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3986,6 +3986,62 @@}}},++ "/api/v1/ui/layout/suggest": {++ "post": {++ "operationId": "post_api_v1_ui_layout_suggest",++ "summary": "Turn a person's words into a ui.layout@1 for one surface, using only the parts its ui.surface@1 catalog has.",++ "description": "**ai.layout.suggest** (first-party): Turn a person's words into a ui.layout@1 for one surface, using only the parts its ui.surface@1 catalog has. The answer is checked by openvibe-sdk/ui-layout against the same catalog; an invalid answer gets one retry with the validator's reasons, then the task says plainly that it could not do it. The suggestion is previewed, never applied. Runs on the cheap structured-output route with the per-person limits every AI run uses, and its receipt names the model. POST /api/v1/ui/layout/suggest with a service token; OpenVibe.Network calls it for the signed-in person and names them in X-OV-Subject.",++ "tags": [++ "ai.layout"++ ],++ "x-openvibe-capabilities": [++ "ai.layout.suggest"++ ],++ "x-openvibe-visibility": [++ "first-party"++ ],++ "x-openvibe-quota-class": "ai-run",++ "security": [++ {++ "openvibeToken": [++ "ai.layout.suggest"++ ]++ }++ ],++ "requestBody": {++ "required": false,++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/ui.layout-request.v1"++ }++ }++ }++ },++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/ui.layout-suggestion.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },"/api/v1/usage": {"get": {"operationId": "get_api_v1_usage",@@@ -10110,6 +10166,750 @@"description": "AgentSubject: an agent (OpenVibe.Actor, or a developer app's agent) acting under grants its owner delegated (roadmap WS-Z2). In an event envelope, on_behalf_of names the owner."}]++ },++ "ui.layout-request.v1": {++ "title": "UiLayoutRequest",++ "description": "ui.layout-request@1: what an editor sends OpenVibe.AI's ui.layout.suggest task, and what Network forwards for a signed-in person. It is the surface's catalog, the layout shown now (or null for the default one) and the words the person typed (max 500 characters), for one device class. The catalog is the only power the answer can use; the current layout is the starting arrangement; the words never contain markup that becomes part of it.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "catalog",++ "layout",++ "words",++ "device"++ ],++ "properties": {++ "catalog": {++ "$ref": "#/components/schemas/ui.surface.v1"++ },++ "layout": {++ "oneOf": [++ {++ "$ref": "#/components/schemas/ui.layout.v1"++ },++ {++ "type": "null"++ }++ ],++ "description": "The arrangement shown now, or null when the person is looking at the default."++ },++ "words": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500,++ "description": "The person's own description, e.g. \"put Stop on the left and make it huge\"."++ },++ "device": {++ "enum": [++ "phone",++ "tablet",++ "computer"++ ]++ }++ }++ },++ "ui.layout-suggestion.v1": {++ "title": "UiLayoutSuggestion",++ "description": "ui.layout-suggestion@1: OpenVibe.AI's answer to ui.layout.suggest. ok true carries the layout, already checked by openvibe-sdk/ui-layout against the catalog sent with the request, and a one-sentence summary of what changed plus the model's reasons. ok false is an honest refusal: no layout, one sentence saying what it could not do, and the validator's reasons in plain words (for example \"Stop has to stay visible.\"). A receipt names the model that answered, with the run and the route. The suggestion is previewed, never applied silently; Network or the product stores it only after the person accepts it.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "ok",++ "layout",++ "summary",++ "reasons"++ ],++ "properties": {++ "ok": {++ "type": "boolean"++ },++ "layout": {++ "oneOf": [++ {++ "$ref": "#/components/schemas/ui.layout.v1"++ },++ {++ "type": "null"++ }++ ],++ "description": "The validated layout when ok is true; null when it is false."++ },++ "summary": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300,++ "description": "One sentence saying what changed, or what could not be done."++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300++ },++ "description": "Why this arrangement, or the validator's reasons for refusing one."++ },++ "receipt": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "model"++ ],++ "properties": {++ "model": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "provider": {++ "type": "string",++ "minLength": 1,++ "maxLength": 100++ },++ "route": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "run_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "cost_usd": {++ "type": "number",++ "minimum": 0++ }++ },++ "description": "Which model answered: the same receipt every AI run carries, at the task's own shape."++ }++ },++ "allOf": [++ {++ "if": {++ "properties": {++ "ok": {++ "const": true++ }++ },++ "required": [++ "ok"++ ]++ },++ "then": {++ "properties": {++ "layout": {++ "type": "object"++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "ok": {++ "const": false++ }++ },++ "required": [++ "ok"++ ]++ },++ "then": {++ "properties": {++ "layout": {++ "type": "null"++ }++ }++ }++ }++ ]++ },++ "ui.layout.v1": {++ "title": "UiLayout",++ "description": "ui.layout@1: one person's arrangement of one surface on one device class. A layout names the surface and its catalog version, the device, its own name, its author and its sharing (private, link or gallery), and one entry per part it places. A part is visible or not, sits on the surface's grid (x, y, w, h in cells) or docks to an edge for an overlay, and carries a style (a theme token tint, a shape, opacity 0.4-1 and the label) and bindings. It is data only: no URL, no markup and no code may appear anywhere in it, and every part id, tint, shape, opacity, size and binding must be one the catalog allows. openvibe-sdk/ui-layout validates it before any server, editor or AI answer stores or shows it.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "surface",++ "surface_version",++ "device",++ "name",++ "author",++ "sharing",++ "parts"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Assigned by OpenVibe.Network when the layout is stored; absent in a draft, a share answer or an AI suggestion."++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "description": "Assigned by the store; every change to a stored layout moves it by one."++ },++ "active": {++ "type": "boolean",++ "description": "Set by OpenVibe.Network: the one layout a product draws for this surface on this device class. A draft, a share answer and an AI suggestion carry no active field."++ },++ "surface": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",++ "maxLength": 80++ },++ "surface_version": {++ "type": "integer",++ "minimum": 1++ },++ "device": {++ "enum": [++ "phone",++ "tablet",++ "computer"++ ]++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "The person's name for this layout; \"My phone controls\" is fine."++ },++ "author": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "name"++ ],++ "properties": {++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "subject": {++ "$ref": "#/components/schemas/identity.subject-ref.v1"++ }++ },++ "description": "The author's display name and, for a private or link layout, the subject. A share or gallery answer carries the display name only, never the account."++ },++ "sharing": {++ "enum": [++ "private",++ "link",++ "gallery"++ ]++ },++ "parts": {++ "type": "array",++ "minItems": 1,++ "maxItems": 64,++ "items": {++ "$ref": "#/components/schemas/ui.layout.v1/$defs/part"++ }++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "$defs": {++ "place": {++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "x",++ "y",++ "w",++ "h"++ ],++ "properties": {++ "x": {++ "type": "integer",++ "minimum": 0,++ "maximum": 23++ },++ "y": {++ "type": "integer",++ "minimum": 0,++ "maximum": 100++ },++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ }++ },++ "description": "The part's cell on the surface grid."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "dock",++ "order",++ "w",++ "h"++ ],++ "properties": {++ "dock": {++ "enum": [++ "left",++ "right",++ "top",++ "bottom"++ ]++ },++ "order": {++ "type": "integer",++ "minimum": 0,++ "maximum": 63++ },++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ }++ },++ "description": "For an overlay: the edge the part docks to, its order along that edge, and its size in cells."++ }++ ]++ },++ "style": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "tint",++ "shape",++ "opacity",++ "label"++ ],++ "properties": {++ "tint": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,39}$",++ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."++ },++ "shape": {++ "enum": [++ "round",++ "pill",++ "square"++ ]++ },++ "opacity": {++ "type": "number",++ "minimum": 0.4,++ "maximum": 1++ },++ "label": {++ "type": "boolean"++ }++ }++ },++ "bindings": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "keys",++ "gamepad"++ ],++ "properties": {++ "keys": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"++ }++ },++ "gamepad": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ }++ }++ }++ },++ "part": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "visible",++ "place",++ "style",++ "bindings"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ },++ "visible": {++ "type": "boolean"++ },++ "place": {++ "$ref": "#/components/schemas/ui.layout.v1/$defs/place"++ },++ "style": {++ "$ref": "#/components/schemas/ui.layout.v1/$defs/style"++ },++ "bindings": {++ "$ref": "#/components/schemas/ui.layout.v1/$defs/bindings"++ }++ }++ }++ }++ },++ "ui.surface.v1": {++ "title": "UiSurface",++ "description": "ui.surface@1: the catalog a product publishes for one surface people may arrange (bot.controls, bot.overlay, live.channel, dock, chat.composer, navbar.shortcuts). It names the surface, its device classes and grid, the theme tokens its styles draw from, and every part a layout may place. A part says what it is (stick, button, slider, readout, video, list), whether it is required or pressed, its minimum cells and the minimum pixels the product guarantees (44 px for anything pressed, larger for Stop), the sizes and styles it allows, the bindings it accepts, and the parts it must never cover. A layout is validated against this catalog (openvibe-sdk/ui-layout); a layout can arrange only what the catalog has and can never add a power. The catalog is data: a layout or an AI answer never carries code or a URL.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "version",++ "name",++ "devices",++ "grid",++ "theme_tokens",++ "parts"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",++ "maxLength": 80,++ "description": "The surface id: <product>.<surface>, e.g. bot.controls."++ },++ "version": {++ "type": "integer",++ "minimum": 1,++ "description": "The catalog version. A layout names the version it was made against."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "What the surface is called where a person sees it."++ },++ "description": {++ "type": "string",++ "maxLength": 500++ },++ "devices": {++ "type": "array",++ "minItems": 1,++ "maxItems": 3,++ "uniqueItems": true,++ "items": {++ "enum": [++ "phone",++ "tablet",++ "computer"++ ]++ },++ "description": "The device classes this surface offers."++ },++ "grid": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "columns",++ "cell_px"++ ],++ "properties": {++ "columns": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "phone",++ "tablet",++ "computer"++ ],++ "description": "The grid's columns per device class: 4 on a phone and 12 on a computer in the design; a product may choose otherwise within these bounds.",++ "properties": {++ "phone": {++ "type": "integer",++ "minimum": 2,++ "maximum": 8++ },++ "tablet": {++ "type": "integer",++ "minimum": 2,++ "maximum": 16++ },++ "computer": {++ "type": "integer",++ "minimum": 2,++ "maximum": 24++ }++ }++ },++ "cell_px": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "phone",++ "tablet",++ "computer"++ ],++ "description": "The smallest a cell is rendered at that device class, in CSS pixels, at the narrowest supported width. The validator multiplies a part's cells by this to check the 44 px rule.",++ "properties": {++ "phone": {++ "type": "integer",++ "minimum": 24,++ "maximum": 200++ },++ "tablet": {++ "type": "integer",++ "minimum": 24,++ "maximum": 200++ },++ "computer": {++ "type": "integer",++ "minimum": 24,++ "maximum": 200++ }++ }++ }++ }++ },++ "theme_tokens": {++ "type": "array",++ "minItems": 1,++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,39}$"++ },++ "description": "The theme tokens a tint may name (accent, surface, danger, ...). Never a colour value: the theme decides what the token is."++ },++ "parts": {++ "type": "array",++ "minItems": 1,++ "maxItems": 64,++ "items": {++ "$ref": "#/components/schemas/ui.surface.v1/$defs/part"++ }++ }++ },++ "$defs": {++ "part": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "name",++ "kind",++ "required",++ "pressed",++ "min",++ "styles",++ "bindings"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$",++ "description": "Stable part id inside this catalog, e.g. stop, stick, battery."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "kind": {++ "enum": [++ "stick",++ "button",++ "slider",++ "readout",++ "video",++ "list"++ ]++ },++ "required": {++ "type": "boolean",++ "description": "A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused."++ },++ "pressed": {++ "type": "boolean",++ "description": "The part sends a command when pressed or moved; it must render at least 44 px on every side."++ },++ "min": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "w",++ "h"++ ],++ "description": "The smallest size a layout may give the part: w and h in grid cells, and px when the product guarantees more than 44 px (Stop).",++ "properties": {++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "px": {++ "type": "integer",++ "minimum": 1,++ "maximum": 400++ }++ }++ },++ "sizes": {++ "type": "array",++ "maxItems": 12,++ "uniqueItems": true,++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "w",++ "h"++ ],++ "properties": {++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ }++ }++ },++ "description": "Sizes the editor may offer as presets. A layout is not limited to these: it is limited to min and to the grid."++ },++ "styles": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "tints",++ "shapes"++ ],++ "properties": {++ "tints": {++ "type": "array",++ "minItems": 1,++ "maxItems": 16,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,39}$"++ },++ "description": "The theme tokens this part may be tinted with; every one is also in the surface's theme_tokens."++ },++ "shapes": {++ "type": "array",++ "minItems": 1,++ "maxItems": 3,++ "uniqueItems": true,++ "items": {++ "enum": [++ "round",++ "pill",++ "square"++ ]++ }++ },++ "opacity": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "min",++ "max"++ ],++ "properties": {++ "min": {++ "type": "number",++ "minimum": 0.4,++ "maximum": 1++ },++ "max": {++ "type": "number",++ "minimum": 0.4,++ "maximum": 1++ }++ },++ "description": "The opacity range this part allows; the design's range is 0.4 to 1."++ },++ "label": {++ "type": "boolean",++ "description": "Whether the editor may show the part's label on it."++ }++ }++ },++ "bindings": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "keys",++ "gamepad"++ ],++ "properties": {++ "keys": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"++ }++ },++ "gamepad": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ }++ }++ },++ "description": "The bindings the product accepts for this part. A layout may pick a subset, never add one."++ },++ "never_covers": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ },++ "description": "The parts this part must never cover, in addition to the surface-wide rule that nothing covers a required part."++ }++ }++ }++ }}},"securitySchemes": {@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Bench API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Billing API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Blog API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Bot API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Chat API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Codes API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Community API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe Connect API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe Connect as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe Connect as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Coupons API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Deals API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Design API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Design as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Design as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Domains API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Events API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Exchange API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Exchange as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Exchange as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Food API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Games API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Git API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Git as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Git as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Host API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -12,8 +12,8 @@"service": "ai","name": "OpenVibe.AI","origin": "https://ai.openvibe.services",-- "operations": 71,-- "capabilities": 18++ "operations": 72,++ "capabilities": 19},{"service": "bench",@@@ -180,8 +180,8 @@"service": "network","name": "OpenVibe.Network","origin": "https://openvibe.network",-- "operations": 73,-- "capabilities": 43++ "operations": 74,++ "capabilities": 44},{"service": "news",@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Inventory API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Live API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Mail API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Mail as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Mail as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.MediaHub API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Media API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Network API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -2057,6 +2057,70 @@}}},++ "/api/v1/people/{subject}/layouts/{surface}": {++ "get": {++ "operationId": "get_api_v1_people_subject_layouts_surface",++ "summary": "Read the active ui.layout@1 one person made for a product's own surface, with that surface's device class.",++ "description": "**network.layout.read** (first-party): Read the active ui.layout@1 one person made for a product's own surface, with that surface's device class. A first-party service presents a service token for openvibe.network and names the person with X-OV-Subject (which must be the person in the path); it reads one layout and nothing else of their account, and never their saved or gallery layouts. The person's own token uses GET /api/v1/me/layouts/:surface instead, which reads and writes only their own rows.",++ "tags": [++ "network.layout"++ ],++ "x-openvibe-capabilities": [++ "network.layout.read"++ ],++ "x-openvibe-visibility": [++ "first-party"++ ],++ "x-openvibe-quota-class": "standard",++ "security": [++ {++ "openvibeToken": [++ "network.layout.read"++ ]++ }++ ],++ "parameters": [++ {++ "name": "subject",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ },++ {++ "name": "surface",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/ui.layout.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },"/api/v1/resources": {"get": {"operationId": "get_api_v1_resources",@@@ -9635,6 +9699,260 @@"format": "date-time"}}++ },++ "ui.layout.v1": {++ "title": "UiLayout",++ "description": "ui.layout@1: one person's arrangement of one surface on one device class. A layout names the surface and its catalog version, the device, its own name, its author and its sharing (private, link or gallery), and one entry per part it places. A part is visible or not, sits on the surface's grid (x, y, w, h in cells) or docks to an edge for an overlay, and carries a style (a theme token tint, a shape, opacity 0.4-1 and the label) and bindings. It is data only: no URL, no markup and no code may appear anywhere in it, and every part id, tint, shape, opacity, size and binding must be one the catalog allows. openvibe-sdk/ui-layout validates it before any server, editor or AI answer stores or shows it.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "surface",++ "surface_version",++ "device",++ "name",++ "author",++ "sharing",++ "parts"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Assigned by OpenVibe.Network when the layout is stored; absent in a draft, a share answer or an AI suggestion."++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "description": "Assigned by the store; every change to a stored layout moves it by one."++ },++ "active": {++ "type": "boolean",++ "description": "Set by OpenVibe.Network: the one layout a product draws for this surface on this device class. A draft, a share answer and an AI suggestion carry no active field."++ },++ "surface": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",++ "maxLength": 80++ },++ "surface_version": {++ "type": "integer",++ "minimum": 1++ },++ "device": {++ "enum": [++ "phone",++ "tablet",++ "computer"++ ]++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "The person's name for this layout; \"My phone controls\" is fine."++ },++ "author": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "name"++ ],++ "properties": {++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "subject": {++ "$ref": "#/components/schemas/identity.subject-ref.v1"++ }++ },++ "description": "The author's display name and, for a private or link layout, the subject. A share or gallery answer carries the display name only, never the account."++ },++ "sharing": {++ "enum": [++ "private",++ "link",++ "gallery"++ ]++ },++ "parts": {++ "type": "array",++ "minItems": 1,++ "maxItems": 64,++ "items": {++ "$ref": "#/components/schemas/ui.layout.v1/$defs/part"++ }++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "$defs": {++ "place": {++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "x",++ "y",++ "w",++ "h"++ ],++ "properties": {++ "x": {++ "type": "integer",++ "minimum": 0,++ "maximum": 23++ },++ "y": {++ "type": "integer",++ "minimum": 0,++ "maximum": 100++ },++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ }++ },++ "description": "The part's cell on the surface grid."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "dock",++ "order",++ "w",++ "h"++ ],++ "properties": {++ "dock": {++ "enum": [++ "left",++ "right",++ "top",++ "bottom"++ ]++ },++ "order": {++ "type": "integer",++ "minimum": 0,++ "maximum": 63++ },++ "w": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ },++ "h": {++ "type": "integer",++ "minimum": 1,++ "maximum": 24++ }++ },++ "description": "For an overlay: the edge the part docks to, its order along that edge, and its size in cells."++ }++ ]++ },++ "style": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "tint",++ "shape",++ "opacity",++ "label"++ ],++ "properties": {++ "tint": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,39}$",++ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."++ },++ "shape": {++ "enum": [++ "round",++ "pill",++ "square"++ ]++ },++ "opacity": {++ "type": "number",++ "minimum": 0.4,++ "maximum": 1++ },++ "label": {++ "type": "boolean"++ }++ }++ },++ "bindings": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "keys",++ "gamepad"++ ],++ "properties": {++ "keys": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"++ }++ },++ "gamepad": {++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ }++ }++ }++ },++ "part": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "visible",++ "place",++ "style",++ "bindings"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"++ },++ "visible": {++ "type": "boolean"++ },++ "place": {++ "$ref": "#/components/schemas/ui.layout.v1/$defs/place"++ },++ "style": {++ "$ref": "#/components/schemas/ui.layout.v1/$defs/style"++ },++ "bindings": {++ "$ref": "#/components/schemas/ui.layout.v1/$defs/bindings"++ }++ }++ }++ }}},"securitySchemes": {@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.News API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenRestream API",-- "version": "0.409.0",-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Play API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Quest API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Report API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Reviews API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Run API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Search API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Services API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Sources API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Space API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tips API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Today API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tools API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Trade API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.VIP API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Vision API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Watch API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Wiki API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Work API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Zone API",-- "version": "0.409.0",-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.410.0",++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -338191,3 +338191,3156 @@ export interface NetworkAgentTaskUpdatedPayload {status: "pending" | "running" | "completed" | "failed" | "cancelled";at: string;}++++/** [email protected] (owner: contracts) */++/**++ * ui.surface@1: the catalog a product publishes for one surface people may arrange (bot.controls, bot.overlay, live.channel, dock, chat.composer, navbar.shortcuts). It names the surface, its device classes and grid, the theme tokens its styles draw from, and every part a layout may place. A part says what it is (stick, button, slider, readout, video, list), whether it is required or pressed, its minimum cells and the minimum pixels the product guarantees (44 px for anything pressed, larger for Stop), the sizes and styles it allows, the bindings it accepts, and the parts it must never cover. A layout is validated against this catalog (openvibe-sdk/ui-layout); a layout can arrange only what the catalog has and can never add a power. The catalog is data: a layout or an AI answer never carries code or a URL.++ */++export interface UiSurface {++ /**++ * The surface id: <product>.<surface>, e.g. bot.controls.++ */++ id: string;++ /**++ * The catalog version. A layout names the version it was made against.++ */++ version: number;++ /**++ * What the surface is called where a person sees it.++ */++ name: string;++ description?: string;++ /**++ * The device classes this surface offers.++ *++ * @minItems 1++ * @maxItems 3++ */++ devices:++ | ["phone" | "tablet" | "computer"]++ | ["phone" | "tablet" | "computer", "phone" | "tablet" | "computer"]++ | ["phone" | "tablet" | "computer", "phone" | "tablet" | "computer", "phone" | "tablet" | "computer"];++ grid: {++ /**++ * The grid's columns per device class: 4 on a phone and 12 on a computer in the design; a product may choose otherwise within these bounds.++ */++ columns: {++ phone: number;++ tablet: number;++ computer: number;++ };++ /**++ * The smallest a cell is rendered at that device class, in CSS pixels, at the narrowest supported width. The validator multiplies a part's cells by this to check the 44 px rule.++ */++ cell_px: {++ phone: number;++ tablet: number;++ computer: number;++ };++ };++ /**++ * The theme tokens a tint may name (accent, surface, danger, ...). Never a colour value: the theme decides what the token is.++ *++ * @minItems 1++ * @maxItems 64++ */++ theme_tokens: [string, ...string[]];++ /**++ * @minItems 1++ * @maxItems 64++ */++ parts: [++ {++ /**++ * Stable part id inside this catalog, e.g. stop, stick, battery.++ */++ id: string;++ name: string;++ kind: "stick" | "button" | "slider" | "readout" | "video" | "list";++ /**++ * A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused.++ */++ required: boolean;++ /**++ * The part sends a command when pressed or moved; it must render at least 44 px on every side.++ */++ pressed: boolean;++ /**++ * The smallest size a layout may give the part: w and h in grid cells, and px when the product guarantees more than 44 px (Stop).++ */++ min: {++ w: number;++ h: number;++ px?: number;++ };++ /**++ * Sizes the editor may offer as presets. A layout is not limited to these: it is limited to min and to the grid.++ *++ * @maxItems 12++ */++ sizes?:++ | []++ | [++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ];++ styles: {++ /**++ * The theme tokens this part may be tinted with; every one is also in the surface's theme_tokens.++ *++ * @minItems 1++ * @maxItems 16++ */++ tints:++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ /**++ * @minItems 1++ * @maxItems 3++ */++ shapes:++ | ["round" | "pill" | "square"]++ | ["round" | "pill" | "square", "round" | "pill" | "square"]++ | ["round" | "pill" | "square", "round" | "pill" | "square", "round" | "pill" | "square"];++ /**++ * The opacity range this part allows; the design's range is 0.4 to 1.++ */++ opacity?: {++ min: number;++ max: number;++ };++ /**++ * Whether the editor may show the part's label on it.++ */++ label?: boolean;++ };++ /**++ * The bindings the product accepts for this part. A layout may pick a subset, never add one.++ */++ bindings: {++ /**++ * @maxItems 32++ */++ keys: string[];++ /**++ * @maxItems 32++ */++ gamepad: string[];++ };++ /**++ * The parts this part must never cover, in addition to the surface-wide rule that nothing covers a required part.++ *++ * @maxItems 32++ */++ never_covers?: string[];++ },++ ...{++ /**++ * Stable part id inside this catalog, e.g. stop, stick, battery.++ */++ id: string;++ name: string;++ kind: "stick" | "button" | "slider" | "readout" | "video" | "list";++ /**++ * A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused.++ */++ required: boolean;++ /**++ * The part sends a command when pressed or moved; it must render at least 44 px on every side.++ */++ pressed: boolean;++ /**++ * The smallest size a layout may give the part: w and h in grid cells, and px when the product guarantees more than 44 px (Stop).++ */++ min: {++ w: number;++ h: number;++ px?: number;++ };++ /**++ * Sizes the editor may offer as presets. A layout is not limited to these: it is limited to min and to the grid.++ *++ * @maxItems 12++ */++ sizes?:++ | []++ | [++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ]++ | [++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ },++ {++ w: number;++ h: number;++ }++ ];++ styles: {++ /**++ * The theme tokens this part may be tinted with; every one is also in the surface's theme_tokens.++ *++ * @minItems 1++ * @maxItems 16++ */++ tints:++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ /**++ * @minItems 1++ * @maxItems 3++ */++ shapes:++ | ["round" | "pill" | "square"]++ | ["round" | "pill" | "square", "round" | "pill" | "square"]++ | ["round" | "pill" | "square", "round" | "pill" | "square", "round" | "pill" | "square"];++ /**++ * The opacity range this part allows; the design's range is 0.4 to 1.++ */++ opacity?: {++ min: number;++ max: number;++ };++ /**++ * Whether the editor may show the part's label on it.++ */++ label?: boolean;++ };++ /**++ * The bindings the product accepts for this part. A layout may pick a subset, never add one.++ */++ bindings: {++ /**++ * @maxItems 32++ */++ keys: string[];++ /**++ * @maxItems 32++ */++ gamepad: string[];++ };++ /**++ * The parts this part must never cover, in addition to the surface-wide rule that nothing covers a required part.++ *++ * @maxItems 32++ */++ never_covers?: string[];++ }[]++ ];++}++++/** [email protected] (owner: contracts) */++/**++ * ui.layout@1: one person's arrangement of one surface on one device class. A layout names the surface and its catalog version, the device, its own name, its author and its sharing (private, link or gallery), and one entry per part it places. A part is visible or not, sits on the surface's grid (x, y, w, h in cells) or docks to an edge for an overlay, and carries a style (a theme token tint, a shape, opacity 0.4-1 and the label) and bindings. It is data only: no URL, no markup and no code may appear anywhere in it, and every part id, tint, shape, opacity, size and binding must be one the catalog allows. openvibe-sdk/ui-layout validates it before any server, editor or AI answer stores or shows it.++ */++export interface UiLayout {++ /**++ * Assigned by OpenVibe.Network when the layout is stored; absent in a draft, a share answer or an AI suggestion.++ */++ id?: string;++ /**++ * Assigned by the store; every change to a stored layout moves it by one.++ */++ revision?: number;++ /**++ * Set by OpenVibe.Network: the one layout a product draws for this surface on this device class. A draft, a share answer and an AI suggestion carry no active field.++ */++ active?: boolean;++ surface: string;++ surface_version: number;++ device: "phone" | "tablet" | "computer";++ /**++ * The person's name for this layout; "My phone controls" is fine.++ */++ name: string;++ /**++ * The author's display name and, for a private or link layout, the subject. A share or gallery answer carries the display name only, never the account.++ */++ author: {++ name: string;++ subject?: SubjectRef;++ };++ sharing: "private" | "link" | "gallery";++ /**++ * @minItems 1++ * @maxItems 64++ */++ parts: [++ {++ id: string;++ visible: boolean;++ place:++ | {++ x: number;++ y: number;++ w: number;++ h: number;++ }++ | {++ dock: "left" | "right" | "top" | "bottom";++ order: number;++ w: number;++ h: number;++ };++ style: {++ /**++ * A theme token, never a colour: the theme decides what it is, so contrast follows the theme.++ */++ tint: string;++ shape: "round" | "pill" | "square";++ opacity: number;++ label: boolean;++ };++ bindings: {++ /**++ * @maxItems 32++ */++ keys: string[];++ /**++ * @maxItems 32++ */++ gamepad: string[];++ };++ },++ ...{++ id: string;++ visible: boolean;++ place:++ | {++ x: number;++ y: number;++ w: number;++ h: number;++ }++ | {++ dock: "left" | "right" | "top" | "bottom";++ order: number;++ w: number;++ h: number;++ };++ style: {++ /**++ * A theme token, never a colour: the theme decides what it is, so contrast follows the theme.++ */++ tint: string;++ shape: "round" | "pill" | "square";++ opacity: number;++ label: boolean;++ };++ bindings: {++ /**++ * @maxItems 32++ */++ keys: string[];++ /**++ * @maxItems 32++ */++ gamepad: string[];++ };++ }[]++ ];++ updated_at?: string;++}++++/** [email protected] (owner: contracts) */++/**++ * ui.layout-request@1: what an editor sends OpenVibe.AI's ui.layout.suggest task, and what Network forwards for a signed-in person. It is the surface's catalog, the layout shown now (or null for the default one) and the words the person typed (max 500 characters), for one device class. The catalog is the only power the answer can use; the current layout is the starting arrangement; the words never contain markup that becomes part of it.++ */++export interface UiLayoutRequest {++ catalog: UiSurface;++ /**++ * The arrangement shown now, or null when the person is looking at the default.++ */++ layout: UiLayout | null;++ /**++ * The person's own description, e.g. "put Stop on the left and make it huge".++ */++ words: string;++ device: "phone" | "tablet" | "computer";++}++++/** [email protected] (owner: contracts) */++/**++ * ui.layout-suggestion@1: OpenVibe.AI's answer to ui.layout.suggest. ok true carries the layout, already checked by openvibe-sdk/ui-layout against the catalog sent with the request, and a one-sentence summary of what changed plus the model's reasons. ok false is an honest refusal: no layout, one sentence saying what it could not do, and the validator's reasons in plain words (for example "Stop has to stay visible."). A receipt names the model that answered, with the run and the route. The suggestion is previewed, never applied silently; Network or the product stores it only after the person accepts it.++ */++export type UiLayoutSuggestion = {++ [k: string]: unknown | undefined;++} & {++ ok: boolean;++ /**++ * The validated layout when ok is true; null when it is false.++ */++ layout: UiLayout | null;++ /**++ * One sentence saying what changed, or what could not be done.++ */++ summary: string;++ /**++ * Why this arrangement, or the validator's reasons for refusing one.++ *++ * @maxItems 20++ */++ reasons:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ /**++ * Which model answered: the same receipt every AI run carries, at the task's own shape.++ */++ receipt?: {++ model: string;++ provider?: string;++ route?: string;++ run_id?: string;++ cost_usd?: number;++ };++};++++/** [email protected] (owner: network) */++/**++ * network.layout.changed v1 (OpenVibe.Network server/layouts, written into network_event_outbox in the transaction that changes a stored layout). One event per change to one person's ui.layout@1: set active, saved, renamed, deleted, shared, unshared, published to the gallery, unpublished, copied from the gallery, or reset to the default. Envelope: subject { type: user, id } and the layout's revision, actor the person, visibility internal. The payload carries no account data and no layout contents, so an open page can announce a change and refetch with the person's own token.++ */++export interface NetworkLayoutChangedPayload {++ surface: string;++ device: "phone" | "tablet" | "computer";++ layout_id: string;++ change:++ | "active"++ | "saved"++ | "renamed"++ | "deleted"++ | "shared"++ | "unshared"++ | "published"++ | "unpublished"++ | "copied"++ | "reset";++ revision: number;++ sharing?: "private" | "link" | "gallery";++}++++/** [email protected] (owner: contracts) */++/**++ * ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop, the e-stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.++ */++export type UiAction =++ | {++ kind: "robot.drive_pulse";++ product: "bot";++ direction: "forward" | "back" | "left" | "right" | "spin_left" | "spin_right";++ ms: number;++ /**++ * Fraction of the robot's allowed speed; omitted means the owner's default for the role.++ */++ speed?: number;++ }++ | {++ kind: "robot.head_preset";++ product: "bot";++ /**++ * A head tilt preset. Bot still checks the robot profile and the owner's limits.++ */++ preset: "up" | "center" | "down";++ }++ | {++ kind: "robot.headlight";++ product: "bot";++ on: boolean;++ }++ | {++ kind: "robot.say";++ product: "bot";++ text: string;++ }++ | {++ kind: "robot.macro";++ product: "bot";++ macro: string;++ }++ | {++ kind: "chat.message";++ product: "chat";++ text: string;++ }++ | {++ kind: "chat.pin";++ product: "chat";++ /**++ * trigger pins the message that caused this action; latest pins the newest chat message.++ */++ source: "trigger" | "latest";++ }++ | {++ kind: "chat.poll";++ product: "chat";++ question: string;++ /**++ * @minItems 2++ * @maxItems 10++ */++ options:++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string];++ }++ | {++ kind: "screen.alert";++ product: "live";++ title: string;++ text?: string;++ }++ | {++ kind: "screen.animation";++ product: "live";++ /**++ * An animation the page or overlay has; an unknown name is refused, never fetched.++ */++ name: string;++ }++ | {++ kind: "screen.confetti";++ product: "live";++ intensity?: "low" | "medium" | "high";++ }++ | {++ kind: "screen.sound";++ product: "live";++ /**++ * A sound from the streamer's own sound list; Live never fetches a URL for it.++ */++ sound: string;++ volume?: number;++ }++ | {++ kind: "screen.tts";++ product: "live";++ text: string;++ /**++ * A voice from the streamer's own voice list.++ */++ voice: string;++ }++ | {++ kind: "screen.counter";++ product: "live";++ key: string;++ by: number;++ }++ | {++ kind: "screen.state";++ product: "live";++ key: string;++ value: boolean | number | string | (number | string)[];++ }++ | {++ kind: "screen.visibility";++ product: "live";++ element: string;++ visible: boolean;++ }++ | {++ kind: "screen.scene";++ product: "live";++ /**++ * A scene the layout has; an unknown scene is refused.++ */++ scene: string;++ }++ | {++ kind: "stream.clip";++ product: "live";++ /**++ * How far back the clip starts, at most 30 seconds.++ */++ seconds?: number;++ }++ | {++ kind: "stream.moment";++ product: "live";++ label?: string;++ }++ | {++ kind: "stream.title";++ product: "live";++ title: string;++ }++ | {++ kind: "stream.category";++ product: "live";++ category: string;++ }++ | {++ kind: "ai.ask";++ product: "ai";++ prompt: string;++ model?: string;++ max_cost_usd?: number;++ }++ | {++ kind: "actor.task";++ product: "actor";++ task: string;++ }++ | {++ kind: "webhook.call";++ product: "live";++ endpoint: string;++ }++ | {++ kind: "money.tip_goal";++ product: "billing";++ goal: string;++ };++++/** [email protected] (owner: contracts) */++/**++ * ui.element@1: an Element people make and place on a surface: a look, a behaviour and, only when a recipe is not enough, a mod. LOOK draws from OpenVibe.Design's components and the theme's tokens, so it follows the person's theme, the seasons and dark/light; it carries no markup and no URL. RECIPE is data only: one or more triggers from ui.trigger@1, the conditions that decide who may fire it and what it costs, and one or more actions from ui.action@1, checked against CAPABILITIES. A capability is an action kind the element declares; the host refuses an action the element did not declare, and the product named by the action still checks the presser's role, the cooldown, the cost and its own limits. MOD is an optional reference to a code module (id, version and digest) for the stage where a recipe is not enough; the module never adds a power. Conditions use channel points only in stage 1: no Vibes, no money, and Billing still confirms any charge itself. The id and revision are assigned by the store; a draft has neither.++ */++export interface UiElement {++ /**++ * Assigned by the store when the element is saved; absent in a draft.++ */++ id?: string;++ /**++ * Assigned by the store; every saved change moves it by one.++ */++ revision?: number;++ name: string;++ description?: string;++ /**++ * The author's display name and, for a private or link element, the subject. A gallery or copy answer carries the display name only, never the account.++ */++ author: {++ name: string;++ subject?: SubjectRef;++ };++ sharing: "private" | "link" | "gallery";++ look: {++ /**++ * An OpenVibe.Design component name (design.component@1), e.g. button or meter. A name the page does not have is refused, never fetched.++ */++ component: string;++ /**++ * The words on the element, e.g. Make it dance.++ */++ label: string;++ /**++ * A glyph name from the design icon set.++ */++ icon?: string;++ /**++ * The design component's own props (the sliders the editor shows). Numbers, strings and booleans only; no nesting.++ */++ props?: {++ [k: string]: (boolean | number | string) | undefined;++ };++ style?: {++ /**++ * A theme token, never a colour: the theme decides what it is, so contrast follows the theme.++ */++ tint?: string;++ shape?: "round" | "pill" | "square";++ opacity?: number;++ show_label?: boolean;++ };++ };++ recipe: {++ /**++ * @minItems 1++ * @maxItems 8++ */++ triggers:++ | [UiTrigger]++ | [UiTrigger, UiTrigger]++ | [UiTrigger, UiTrigger, UiTrigger]++ | [UiTrigger, UiTrigger, UiTrigger, UiTrigger]++ | [UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger]++ | [UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger]++ | [UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger]++ | [UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger];++ conditions?: {++ /**++ * Who may fire it. A higher role admits the lower ones where the product says so (an operator is admitted to subscriber-only); owner is the channel's owner alone. Default everyone.++ */++ role?: "everyone" | "follower" | "subscriber" | "vip" | "operator" | "owner";++ /**++ * Channel points the presser spends, from Live's points. Stage 1 has no other currency, and an element never charges money.++ */++ cost?: {++ points: number;++ };++ /**++ * How long the same person must wait, and how long everybody must wait, before it can fire again.++ */++ cooldown?: {++ per_person_seconds?: number;++ overall_seconds?: number;++ };++ /**++ * The most times it may fire in a UTC day, per person and for everyone.++ */++ daily_cap?: {++ per_person?: number;++ overall?: number;++ };++ /**++ * When true the element fires only while the channel is live.++ */++ live_only?: boolean;++ /**++ * When true a robot action is requested only while the presser has the robot's turn; Bot still decides.++ */++ robot_turn_only?: boolean;++ /**++ * Fire only one time in N. The host draws the lot, so a refused draw costs nothing.++ */++ chance?: {++ one_in: number;++ };++ };++ /**++ * @minItems 1++ * @maxItems 16++ */++ actions:++ | [UiAction]++ | [UiAction, UiAction]++ | [UiAction, UiAction, UiAction]++ | [UiAction, UiAction, UiAction, UiAction]++ | [UiAction, UiAction, UiAction, UiAction, UiAction]++ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]++ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]++ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]++ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]++ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]++ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]++ | [++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction++ ]++ | [++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction++ ]++ | [++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction++ ]++ | [++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction++ ]++ | [++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction,++ UiAction++ ];++ };++ /**++ * The action kinds this element may request. Every action in the recipe must be declared here; the host refuses the rest.++ *++ * @minItems 1++ * @maxItems 32++ */++ capabilities: [++ (++ | "robot.drive_pulse"++ | "robot.head_preset"++ | "robot.headlight"++ | "robot.say"++ | "robot.macro"++ | "chat.message"++ | "chat.pin"++ | "chat.poll"++ | "screen.alert"++ | "screen.animation"++ | "screen.confetti"++ | "screen.sound"++ | "screen.tts"++ | "screen.counter"++ | "screen.state"++ | "screen.visibility"++ | "screen.scene"++ | "stream.clip"++ | "stream.moment"++ | "stream.title"++ | "stream.category"++ | "ai.ask"++ | "actor.task"++ | "webhook.call"++ | "money.tip_goal"++ ),++ ...(++ | "robot.drive_pulse"++ | "robot.head_preset"++ | "robot.headlight"++ | "robot.say"++ | "robot.macro"++ | "chat.message"++ | "chat.pin"++ | "chat.poll"++ | "screen.alert"++ | "screen.animation"++ | "screen.confetti"++ | "screen.sound"++ | "screen.tts"++ | "screen.counter"++ | "screen.state"++ | "screen.visibility"++ | "screen.scene"++ | "stream.clip"++ | "stream.moment"++ | "stream.title"++ | "stream.category"++ | "ai.ask"++ | "actor.task"++ | "webhook.call"++ | "money.tip_goal"++ )[]++ ];++ /**++ * The element's state before anything runs; the counter starts at 0, never at a value the element did not declare.++ */++ initial_state?: {++ [k: string]: (boolean | number | string | (number | string)[]) | undefined;++ };++ /**++ * A code module for an element whose recipe is not enough (stage 3). It runs sandboxed; the reference is data, and the host checks every request the module makes against the element's capabilities.++ */++ mod?: {++ id: string;++ version: string;++ sha256: string;++ /**++ * The subset of the element's capabilities the module itself may request; omitted means the element's declared capabilities, and a product may grant fewer.++ *++ * @maxItems 32++ */++ capabilities?: (++ | "robot.drive_pulse"++ | "robot.head_preset"++ | "robot.headlight"++ | "robot.say"++ | "robot.macro"++ | "chat.message"++ | "chat.pin"++ | "chat.poll"++ | "screen.alert"++ | "screen.animation"++ | "screen.confetti"++ | "screen.sound"++ | "screen.tts"++ | "screen.counter"++ | "screen.state"++ | "screen.visibility"++ | "screen.scene"++ | "stream.clip"++ | "stream.moment"++ | "stream.title"++ | "stream.category"++ | "ai.ask"++ | "actor.task"++ | "webhook.call"++ | "money.tip_goal"++ )[];++ };++ created_at?: string;++ updated_at?: string;++}++++/** [email protected] (owner: contracts) */++/**++ * ui.element-state@1: the small store an Element keeps for itself (counters, scores, lists, a wheel's entries), as Live broadcasts it to everyone watching and as a read returns it. CHANNEL and ELEMENT_ID name the element instance, REVISION grows by one with each accepted change so a late frame never overwrites a newer one, and VALUES is the store itself: at most 32 keys, each a boolean, a number, a short string or a list of those. It is state, not code: no objects, no markup, no URL and no account data, which keeps a shared element's state safe to render for every viewer.++ */++export interface UiElementState {++ /**++ * The Live channel's username (its slug).++ */++ channel: string;++ element_id: string;++ revision: number;++ /**++ * The element's own store. A key is lower-case and 32 characters at most; a value is a boolean, a number, a string of 200 characters at most or a list of those.++ */++ values: {++ [k: string]: (boolean | number | string | (number | string)[]) | undefined;++ };++ updated_at: string;++}++++/** [email protected] (owner: contracts) */++/**++ * ui.trigger@1: one trigger an Element's recipe listens for, from the fixed catalog. kind says what happened: a viewer pressing, holding or double-pressing the element, a key or gamepad binding, a chat command or text, a follow, a tip, an AI Moment, a Rundown topic, a timer or schedule, robot telemetry or an event, the stream starting or ending, a viewer-count crossing or another element's event. The fields a kind needs are the only fields it accepts. A trigger is data: it never names code or a URL, and a product may refuse a kind it does not offer yet. Stage 1 of Elements uses press and chat_command; the rest are declared here so a stage-2 element and the editor speak one language.++ */++export type UiTrigger =++ | {++ kind: "press";++ }++ | {++ kind: "hold";++ ms: number;++ }++ | {++ kind: "double_press";++ /**++ * The longest gap between the two presses; 400 by default.++ */++ within_ms?: number;++ }++ | {++ kind: "key";++ key: string;++ /**++ * @maxItems 4++ */++ modifiers?:++ | []++ | ["ctrl" | "alt" | "shift" | "meta"]++ | ["ctrl" | "alt" | "shift" | "meta", "ctrl" | "alt" | "shift" | "meta"]++ | ["ctrl" | "alt" | "shift" | "meta", "ctrl" | "alt" | "shift" | "meta", "ctrl" | "alt" | "shift" | "meta"]++ | [++ "ctrl" | "alt" | "shift" | "meta",++ "ctrl" | "alt" | "shift" | "meta",++ "ctrl" | "alt" | "shift" | "meta",++ "ctrl" | "alt" | "shift" | "meta"++ ];++ }++ | {++ kind: "gamepad";++ button: string;++ /**++ * Which connected pad; omitted means any.++ */++ index?: number;++ }++ | {++ kind: "chat_command";++ /**++ * The command without arguments, lower case, e.g. !dance.++ */++ command: string;++ }++ | {++ kind: "chat_text";++ /**++ * @minItems 1++ * @maxItems 16++ */++ words:++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ /**++ * any (default) matches one of the words; all matches every one.++ */++ mode?: "any" | "all";++ }++ | {++ kind: "follow";++ }++ | {++ kind: "tip";++ /**++ * The smallest tip, in Vibes, that fires the trigger; omitted means any tip.++ */++ min?: number;++ }++ | {++ kind: "ai_moment";++ }++ | {++ kind: "rundown_topic";++ /**++ * Optional words the topic has to mention; omitted means any topic.++ *++ * @maxItems 8++ */++ words?:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string];++ }++ | {++ kind: "timer";++ every_seconds: number;++ /**++ * live (default) counts only while the channel is live; always counts from the element being active.++ */++ start?: "live" | "always";++ }++ | {++ kind: "schedule";++ /**++ * A five-field cron in the person's timezone.++ */++ cron: string;++ timezone: string;++ }++ | {++ kind: "robot_telemetry";++ metric: "battery_percent" | "latency_ms" | "stuck" | "temperature_c";++ op: "lt" | "gt";++ value: number;++ /**++ * How long the condition must hold; 0 (default) means at once.++ */++ for_seconds?: number;++ }++ | {++ kind: "robot_event";++ event: "estop" | "stopped" | "disconnected" | "command_refused" | "button";++ }++ | {++ kind: "stream_start";++ }++ | {++ kind: "stream_end";++ }++ | {++ kind: "viewer_count";++ op: "above" | "below";++ count: number;++ }++ | {++ kind: "element_event";++ element_id: string;++ /**++ * pressed (default) fires after that element ran; state and shown fire when Live broadcasts them.++ */++ event?: "pressed" | "state" | "shown";++ };++++/** [email protected] (owner: live) */++/**++ * live.element.pressed v1 (OpenVibe.Live server/elements): a viewer's element press was accepted and ran. It carries the public facts the stream should see: the channel, the element's id and name, the presser's public name and subject, the trigger that fired, the channel points it cost and the element-state revision it left behind. It never carries the presser's email, address, account balance or anything private; a refused press does not produce it. Envelope: subject { type: element, id: el_… }, actor the presser's subject, visibility public, priority low. Also written to the channel's AI timeline as a moment, so the stream's own timeline can cite 'chat made the robot dance'.++ */++export interface LiveElementPressedPayload {++ /**++ * The public channel facts (the channel page is public already).++ */++ channel: {++ username: string;++ display_name: string;++ };++ element_id: string;++ element_name: string;++ presser: SubjectRef;++ /**++ * The display name the stream shows, never an email or an account id.++ */++ presser_name: string;++ trigger: UiTrigger;++ /**++ * Channel points the press cost; 0 when it was free.++ */++ points_spent?: number;++ /**++ * The element-state revision after the actions ran; 0 when the element has no state yet.++ */++ state_revision?: number;++ at: string;++}++++/** [email protected] (owner: live) */++/**++ * live.element.state v1 (OpenVibe.Live server/elements): an element's own store changed, broadcast to everyone watching through Live's existing events path. Carries the channel's public facts and the same store as ui.element-state@1: the element instance, a revision that grows by one per accepted change, the values themselves and when. It is public state (a counter, a score, a wheel's entries), never the presser's data: a value may not be an object, a markup string or a URL, and the payload carries no account.++ */++export interface LiveElementStatePayload {++ channel: {++ username: string;++ display_name: string;++ };++ element_id: string;++ revision: number;++ /**++ * The element's own store. A key is lower-case and 32 characters at most; a value is a boolean, a number, a string of 200 characters at most or a list of those.++ */++ values: {++ [k: string]: (boolean | number | string | (number | string)[]) | undefined;++ };++ updated_at: string;++}++++/** [email protected] (owner: ai) */++/**++ * A deep dive: expert design research run by OpenVibe.AI (workflow design.deep_dive) for OpenVibe.Exchange's /deep-dive page and, through the API, OpenVibe.Design. One document holds the question and depth (the input), the seven steps, every source read, every capture (a private Media object per viewport and theme, with its DOM facts and Expert extras), the per-site analysis, the synthesis across sites, the recommendations checked against OpenVibe's own rules, and the receipt of every model call and capture. Every claim carries its evidence: the site, the capture or crop, the URL and the date it was read. Nothing is copied: captures are private to the run's owner and the report shows them as attributed commentary.++ */++export interface AiDeepDive {++ /**++ * The run id; a deep dive is a run (GET /api/v1/deep-dives/{id}).++ */++ id: string;++ schema: "ai.deep-dive@1";++ run_id: string;++ status: "queued" | "running" | "choosing" | "succeeded" | "failed" | "cancelled";++ input: {++ question: string;++ depth: "quick" | "thorough" | "expert";++ max_sites: number;++ /**++ * Sites the person named when starting the run.++ *++ * @maxItems 30++ */++ sites?: string[];++ };++ steps: {++ key: "plan" | "search" | "choose" | "capture" | "analyse" | "synthesise" | "report";++ status: "pending" | "running" | "done" | "skipped" | "failed";++ detail?: string | null;++ started_at?: string | null;++ finished_at?: string | null;++ }[];++ sources: {++ id: string;++ kind: "web" | "picture" | "icon" | "project" | "rules" | "component" | "named";++ title: string;++ url: string;++ publisher?: string | null;++ engine?: string | null;++ snippet?: string | null;++ license?: string | null;++ read_at: string;++ }[];++ captures: {++ id: string;++ source_id: string;++ url: string;++ final_url?: string | null;++ title?: string | null;++ captured_at: string;++ viewport: 390 | 1280;++ theme: "light" | "dark";++ full_page: boolean;++ media_id?: string | null;++ width?: number | null;++ height?: number | null;++ crops: {++ id: string;++ media_id: string;++ y: number;++ height: number;++ }[];++ dom?: {} | null;++ extras?: {} | null;++ /**++ * Why the capture did not happen: robots_disallowed, not_public, no_chrome, timeout, …++ */++ skip?: string | null;++ cached?: boolean;++ }[];++ analysis: {++ source_id: string;++ url: string;++ summary: string;++ first_impression?: string;++ hierarchy?: string;++ density?: string;++ type?: string;++ colour?: string;++ motion?: string;++ words?: string;++ /**++ * @maxItems 12++ */++ strengths:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string];++ /**++ * @maxItems 12++ */++ weaknesses:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string];++ accessibility?: string | null;++ speed?: string | null;++ evidence: {++ claim: string;++ source_id?: string | null;++ capture_id?: string | null;++ crop_id?: string | null;++ url: string;++ read_at: string;++ }[];++ }[];++ synthesis: {++ patterns: {++ name: string;++ what: string;++ /**++ * @maxItems 30++ */++ sites: string[];++ usage_count?: number;++ evidence: {++ claim: string;++ source_id?: string | null;++ capture_id?: string | null;++ crop_id?: string | null;++ url: string;++ read_at: string;++ }[];++ }[];++ comparison: {++ /**++ * @maxItems 12++ */++ columns:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string];++ rows:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string][];++ };++ best: {++ source_id: string;++ what: string;++ evidence: {++ claim: string;++ source_id?: string | null;++ capture_id?: string | null;++ crop_id?: string | null;++ url: string;++ read_at: string;++ }[];++ };++ mistakes: {++ what: string;++ evidence: {++ claim: string;++ source_id?: string | null;++ capture_id?: string | null;++ crop_id?: string | null;++ url: string;++ read_at: string;++ }[];++ }[];++ gap: string;++ reference_tokens: {++ palettes: {++ source_id: string;++ /**++ * @maxItems 16++ */++ colours:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ }[];++ type_scales: {++ source_id: string;++ /**++ * @maxItems 8++ */++ families:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string];++ /**++ * @maxItems 16++ */++ sizes:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ }[];++ spacing: {++ source_id: string;++ /**++ * @maxItems 16++ */++ steps:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ }[];++ };++ /**++ * @maxItems 20++ */++ do:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ /**++ * @maxItems 20++ */++ dont:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ /**++ * @maxItems 20++ */++ accessibility:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ /**++ * @maxItems 20++ */++ speed:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ };++ recommendations: {++ id: string;++ what: string;++ why: string;++ rule: {++ id?: string | null;++ status: "follows" | "would_change" | "none";++ text: string;++ };++ evidence: {++ claim: string;++ source_id?: string | null;++ capture_id?: string | null;++ crop_id?: string | null;++ url: string;++ read_at: string;++ }[];++ }[];++ receipt: {++ price_usd: number;++ held_usd: number;++ cost_usd: number;++ captures_count: number;++ synthetic: boolean;++ models: {++ step: "plan" | "choose" | "analyse" | "synthesise";++ provider: string | null;++ model: string | null;++ tokens_in: number;++ tokens_out: number;++ cost_usd: number;++ ms: number;++ }[];++ };++ /**++ * @maxItems 40++ */++ gaps: string[];++ created_at: string;++ updated_at: string;++ finished_at?: string | null;++}++++/** [email protected] (owner: ai) */++/**++ * ai.deep_dive.updated v1 (OpenVibe.AI server/deep-dive): a deep dive's step changed or it ended. One event per step transition and one at the end, so a page (OpenVibe.Exchange's /deep-dive) can follow a run without polling, and OpenVibe.Design can attach a finished report. The payload carries the run and deep-dive ids, the step, the progress and the status; never the question's answer, a capture or a prompt. Envelope: subject { type: run, id: <run_id> }, visibility internal, priority low (important on failed), actor service:ai.++ */++export interface AiDeepDiveUpdatedPayload {++ deep_dive_id: string;++ run_id: string;++ status: "queued" | "running" | "choosing" | "succeeded" | "failed" | "cancelled";++ step: "plan" | "search" | "choose" | "capture" | "analyse" | "synthesise" | "report" | "done" | null;++ progress: number;++ steps: {++ key: "plan" | "search" | "choose" | "capture" | "analyse" | "synthesise" | "report";++ status: "pending" | "running" | "done" | "skipped" | "failed";++ }[];++ depth: "quick" | "thorough" | "expert";++ requester: SubjectRef;++ on_behalf_of: SubjectRef | null;++ url: string;++ error: {++ code: string;++ detail: string;++ } | null;++ occurred_at: string;++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "ai.layout.suggest",++ "version": "1.0",++ "owner": "ai",++ "status": "active",++ "visibility": "first-party",++ "description": "Turn a person's words into a ui.layout@1 for one surface, using only the parts its ui.surface@1 catalog has. The answer is checked by openvibe-sdk/ui-layout against the same catalog; an invalid answer gets one retry with the validator's reasons, then the task says plainly that it could not do it. The suggestion is previewed, never applied. Runs on the cheap structured-output route with the per-person limits every AI run uses, and its receipt names the model. POST /api/v1/ui/layout/suggest with a service token; OpenVibe.Network calls it for the signed-in person and names them in X-OV-Subject.",++ "permissions": [++ "ai:layout"++ ],++ "resourceConstraints": [++ "owner"++ ],++ "quotaClass": "ai-run",++ "events": [],++ "implementedBy": [++ "POST /api/v1/ui/layout/suggest"++ ],++ "inputSchema": "ui.layout-request@1",++ "outputSchema": "ui.layout-suggestion@1"++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "network.layout.read",++ "version": "1.0",++ "owner": "network",++ "status": "active",++ "visibility": "first-party",++ "description": "Read the active ui.layout@1 one person made for a product's own surface, with that surface's device class. A first-party service presents a service token for openvibe.network and names the person with X-OV-Subject (which must be the person in the path); it reads one layout and nothing else of their account, and never their saved or gallery layouts. The person's own token uses GET /api/v1/me/layouts/:surface instead, which reads and writes only their own rows.",++ "permissions": [++ "network:layout"++ ],++ "resourceConstraints": [++ "owner"++ ],++ "quotaClass": "standard",++ "events": [],++ "implementedBy": [++ "GET /api/v1/people/:subject/layouts/:surface"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "ui.layout@1"++}@@@ -36,6 +36,7 @@"ai.memory.write","ai.me.read","ai.me.manage",++ "ai.layout.suggest","ai.config.manage","exchange.search.icons"],@@@ -45,7 +46,8 @@"ai.run.succeeded","ai.run.failed","ai.run.cached",-- "ai.credential.failed"++ "ai.credential.failed",++ "ai.deep_dive.updated"],"eventsConsumed": [],"namespacesOwned": [@@@ -46,7 +46,12 @@],"neighbourhood": "watch","strip": [-- { "label": "The Rundown", "icon": "news", "link": "https://openvibe.live/rundown", "line": "What streamers talked about" }++ {++ "label": "The Rundown",++ "icon": "news",++ "link": "https://openvibe.live/rundown",++ "line": "What streamers talked about"++ }]},"capabilities": [@@@ -69,6 +74,8 @@],"eventsProduced": ["live.agent_stream.changed",++ "live.element.pressed",++ "live.element.state","live.index_document.deleted","live.index_document.upserted","live.moderation.action",@@@ -93,7 +93,8 @@"network.levels.award","network.link.access.request","network.agents.register",-- "network.agents.relay"++ "network.agents.relay",++ "network.layout.read"],"eventsProduced": ["network.account.deleted",@@@ -120,6 +121,7 @@"network.machine.paired","network.machine.roles_changed","network.machine.revoked",++ "network.layout.changed","share.granted","share.revoked","share.transfer.requested",@@@ -1,12 +1,12 @@{"name": "openvibe-contracts",-- "version": "0.409.0",++ "version": "0.410.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-contracts",-- "version": "0.409.0",++ "version": "0.410.0","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1,6 +1,6 @@{"name": "openvibe-contracts",-- "version": "0.409.0",++ "version": "0.410.0","description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.","main": "index.js","types": "generated/typescript/index.d.ts",nnew file mode 100644@@@ -0,0 +1,86 @@++'use strict';++// Elements (owner ask 2026-11-11): the four contracts and two events that make a button people++// make. JSON Schema holds most of it; these are the rules it cannot state, and the drift between++// the one action catalog and the capabilities an element declares.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const contracts = require('..');++++const ROOT = path.join(__dirname, '..');++let n = 0;++const ok = (cond, msg) => { assert.ok(cond, msg); n++; };++const fixture = (id, kind, file) => JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures', id, kind, file), 'utf8'));++++// ── The action catalog is one list ───────────────────────────────────────++const actions = contracts.schema('ui.action@1');++const variants = actions.oneOf;++const kinds = variants.map((v) => v.properties.kind.const);++ok(new Set(kinds).size === kinds.length, 'every action kind appears once');++const capability = actions.$defs.capability.enum;++ok(JSON.stringify([...capability].sort()) === JSON.stringify([...kinds].sort()),++ 'ui.action.$defs.capability is exactly the action catalog (an element cannot declare a kind no action has)');++// The product that authorises each kind is fixed by the catalog, not chosen by the element.++const PRODUCT = {++ 'robot.drive_pulse': 'bot', 'robot.head_preset': 'bot', 'robot.headlight': 'bot', 'robot.say': 'bot', 'robot.macro': 'bot',++ 'chat.message': 'chat', 'chat.pin': 'chat', 'chat.poll': 'chat',++ 'screen.alert': 'live', 'screen.animation': 'live', 'screen.confetti': 'live', 'screen.sound': 'live',++ 'screen.tts': 'live', 'screen.counter': 'live', 'screen.state': 'live', 'screen.visibility': 'live',++ 'screen.scene': 'live', 'stream.clip': 'live', 'stream.moment': 'live', 'stream.title': 'live',++ 'stream.category': 'live', 'webhook.call': 'live',++ 'ai.ask': 'ai', 'actor.task': 'actor', 'money.tip_goal': 'billing',++};++ok(Object.keys(PRODUCT).length === kinds.length, 'the product map covers every action kind');++for (const v of variants) {++ const kind = v.properties.kind.const;++ ok(v.properties.product.const === PRODUCT[kind], `${kind} is authorised by ${PRODUCT[kind]}`);++}++++// ── The trigger catalog is one list ──────────────────────────────────────++const triggers = contracts.schema('ui.trigger@1').oneOf;++const triggerKinds = triggers.map((v) => v.properties.kind.const);++ok(new Set(triggerKinds).size === triggerKinds.length, 'every trigger kind appears once');++ok(triggerKinds.includes('press') && triggerKinds.includes('chat_command'),++ 'stage 1\'s triggers (press, chat_command) are in the catalog');++++// ── An element's recipe may only use what it declared ────────────────────++// JSON Schema can compare a capability with an action only inside one document; this is the++// cross-field rule openvibe-sdk/elements' validate() enforces too, checked here on the fixtures.++for (const f of fs.readdirSync(path.join(ROOT, 'fixtures/ui.element/valid')).filter((x) => x.endsWith('.json'))) {++ const el = fixture('ui.element', 'valid', f);++ const declared = new Set(el.capabilities);++ for (const a of el.recipe.actions) ok(declared.has(a.kind), `${f}: ${a.kind} is declared`);++ if (el.mod && el.mod.capabilities) for (const c of el.mod.capabilities) ok(declared.has(c), `${f}: the mod may request only ${c} if the element declared it`);++ const text = JSON.stringify(el);++ ok(!/https?:\/\/|<[a-z!/]/i.test(text), `${f}: an element carries no URL or markup`);++ const state = el.initial_state || {};++ for (const [k, v] of Object.entries(state)) {++ ok(/^[a-z][a-z0-9_]{0,31}$/.test(k), `${f}: state key ${k} is lower-case`);++ ok(v === null || typeof v !== 'object' || Array.isArray(v), `${f}: state ${k} is a value, not an object`);++ }++}++++// ── The store is state, not data that can run ────────────────────────────++const state = contracts.schema('ui.element-state@1');++ok(!contracts.validate('ui.element-state@1', { channel: 'ada', element_id: 'el_01JAB2C3D4E5F6G7H8J9K0MNPQ', revision: 0, values: { wheel: { a: 1 } }, updated_at: '2026-11-11T20:00:00.000Z' }).valid,++ 'a state value may not be an object');++ok(!contracts.validate('ui.element-state@1', { channel: 'ada', element_id: 'el_01JAB2C3D4E5F6G7H8J9K0MNPQ', revision: 0, values: { ok: 'x'.repeat(201) }, updated_at: '2026-11-11T20:00:00.000Z' }).valid,++ 'a state string is at most 200 characters (the product content-checks the text it shows)');++ok(state.properties.values.$ref === '#/$defs/values', 'ui.element-state@1 values are the shared values definition');++++// ── The events carry the public facts only ───────────────────────────────++const pressed = contracts.schema('live.element.pressed@1');++for (const k of ['channel', 'element_id', 'element_name', 'presser', 'presser_name', 'trigger', 'at']) {++ ok(pressed.required.includes(k), `live.element.pressed carries ${k}`);++}++ok(!('email' in pressed.properties) && !('ip' in pressed.properties) && !('balance' in pressed.properties),++ 'live.element.pressed carries no private account data');++const changed = contracts.schema('live.element.state@1');++ok(JSON.stringify(changed.required) === JSON.stringify(['channel', 'element_id', 'revision', 'values', 'updated_at']),++ 'live.element.state names the store and its revision');++for (const id of ['live.element.pressed', 'live.element.state']) {++ const entry = contracts.catalog.find((c) => c.id === id);++ ok(entry && entry.owner === 'live', `${id} is owned by Live`);++}++++console.log(`elements: ${n} checks passed`);@@@ -42,6 +42,7 @@ sub(path.join(ROOT, 'test/compare-comparison.test.js'));sub(path.join(ROOT, 'test/link-brand.test.js'));sub(path.join(ROOT, 'test/link-compute.test.js'));sub(path.join(ROOT, 'test/money-reasons.test.js'));++sub(path.join(ROOT, 'test/elements.test.js'));// ── Catalog integrity ────────────────────────────────────────────────────const files = [];@@@ -141,6 +142,33 @@ ok(contracts.resolve('ai.credential-put@1').schema === 'ai/credential-put.v1.jso&& contracts.resolve('ai.credential-put').version === '2.0.0', 'a contract with two majors resolves by id (newest) or by major');assert.throws(() => contracts.assertValid('identity.subject-ref', { type: 'user', id: '1' }), e => e.code === 'contract.invalid');++// ── UI surfaces and layouts (make your own controls, 2026-11-11) ─────────++// ui.surface@1 is the catalog a product publishes; ui.layout@1 arranges only what it has. The++// catalog's own consistency is not something JSON Schema can state: a part's tint has to be one of++// the surface's theme tokens, part ids are unique, and a never_covers target is a real part.++{++ const surface = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/ui.surface/valid/bot-controls.json'), 'utf8'));++ const partIds = new Set(surface.parts.map((p) => p.id));++ ok(partIds.size === surface.parts.length, 'ui.surface part ids are unique');++ for (const p of surface.parts) {++ for (const tint of p.styles.tints) ok(surface.theme_tokens.includes(tint), `${surface.id}/${p.id} tint ${tint} is a theme token`);++ for (const target of p.never_covers || []) ok(partIds.has(target), `${surface.id}/${p.id} never_covers names ${target}`);++ const cols = surface.grid.columns.phone;++ ok(p.min.w <= cols, `${surface.id}/${p.id} minimum fits the phone grid`);++ if (p.pressed) ok(p.min.w * surface.grid.cell_px.phone >= 44 && p.min.h * surface.grid.cell_px.phone >= 44, `${surface.id}/${p.id} pressed minimum is at least 44 px`);++ }++ // The layout is data only: no URL or markup may appear in a value a layout stores.++ const layoutText = fs.readFileSync(path.join(ROOT, 'fixtures/ui.layout/valid/phone-controls.json'), 'utf8');++ ok(!/https?:\/\/|<[a-z]/i.test(layoutText), 'ui.layout carries no URL or markup');++ // A suggestion says what it could not do rather than carrying an unchecked layout.++ const suggestion = contracts.validate('ui.layout-suggestion@1', JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/ui.layout-suggestion/valid/suggestion.json'), 'utf8')));++ ok(suggestion.valid, 'ui.layout-suggestion accepts a validated arrangement with its reasons and receipt');++ ok(!contracts.validate('ui.layout-suggestion@1', { ok: true, layout: null, summary: 'x', reasons: [] }).valid, 'a successful suggestion must carry a layout');++ // The event names the change and the layout revision; no account data is in the payload.++ const changed = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/network.layout.changed/valid/activated.json'), 'utf8'));++ ok(!('subject' in changed) && !('author' in changed) && !('parts' in changed), 'network.layout.changed carries no account data or layout contents');++}++// ── Manifests ────────────────────────────────────────────────────────────const serviceIds = new Set(services.manifests.map(m => m.id));for (const m of services.manifests) {
@@ -1,5 +1,43 @@
@@ -1,5 +1,43 @@
# Changelog
# Changelog
+## 0.410.0 — 2026-11-11
+
+A train of three pull requests: the layouts of make-your-own-controls (#455), Elements stage 1 (#456) and the deep dive
+(#457).
+
+- Add `ui.surface@1` (the catalog of a surface's parts, their minimum sizes, styles, bindings and
+ `never_covers` rules), `ui.layout@1` (a person's arrangement on one device class, with its name, author and
+ sharing), `ui.layout-request@1` (catalog, current layout, words and device for the AI task) and
+ `ui.layout-suggestion@1` (a validated layout with a one-sentence summary, reasons and the model's receipt, or
+ an honest refusal with the validator's reasons).
+- Add `network.layout.changed@1`: the event Network writes in the transaction that sets, saves, renames, deletes,
+ shares, publishes, copies or resets one person's layout. It carries the surface, device, layout id, change and
+ revision, never account data or layout contents.
+- Add `ai.layout.suggest` (first-party, `POST /api/v1/ui/layout/suggest`) and `network.layout.read` (first-party,
+ `GET /api/v1/people/:subject/layouts/:surface`), and list them in the AI and Network manifests.
+- The fixtures cover a bot-controls catalog, a phone layout, a docked overlay, a suggestion and a refusal, a
+ request and the event; the suite checks part ids, theme tokens, never_covers targets and the 44 px minimum. (#455)
+- Add `ui.element@1`: a look from OpenVibe.Design's components and the theme's tokens, a recipe (triggers, conditions,
+ actions), the capabilities the element declares, an optional mod reference (id, version, digest) and its sharing.
+ The store assigns the id and revision; a draft has neither.
+- Add `ui.trigger@1` (press, hold, double-press, key, gamepad, chat command and text, follow, tip, AI Moment, Rundown
+ topic, timer, schedule, robot telemetry and event, stream start and end, viewer count, another element's event) and
+ `ui.action@1` (robot, chat, screen, stream, AI, Actor, webhook and tip-goal actions), each action carrying the product
+ that authorises it. `ui.action@1`'s `$defs.capability` is the one list an element's `capabilities` draws from.
+- Add `ui.element-state@1`: the element's own store (counters, scores, lists), with a revision that grows per accepted
+ change and values that are booleans, numbers, short strings or lists of them, never objects.
+- Add `live.element.pressed@1` and `live.element.state@1`: a press with the presser's public name and the trigger, and
+ the store with its revision, written by Live.
+- Fixtures cover a gallery dance button, a chat counter, a mod element, trigger and action variants, the state, and
+ both events; `test/elements.test.js` holds the capability list to the action catalog, each kind to its product, and
+ the recipe to the element's declared capabilities. (#456)
+- Add `ai.deep-dive@1`: a deep dive run's report as OpenVibe.AI's `design.deep_dive` workflow produces it for
+ OpenVibe.Exchange and OpenVibe.Design — the question and depth, the seven steps, every source, every capture (a
+ private Media object per viewport and theme, with its DOM facts and Expert extras), the per-site analysis, the
+ synthesis, the recommendations checked against OpenVibe's rules, and the receipt. Every claim carries its evidence.
+- Add `ai.deep_dive.updated@1`, the event AI writes on each step transition and at the end, so a page can follow a
+ run without polling and Design can attach a finished report. (#457)
+
## 0.409.0 — 2026-11-11
## 0.409.0 — 2026-11-11
- Define machine access requests, grants and signed token claims; the agent directory, A2A tasks and results; five Network events and four registration and relay capabilities.
- Define machine access requests, grants and signed token claims; the agent directory, A2A tasks and results; five Network events and four registration and relay capabilities.
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.services/llms.txt
**Docs:** https://openvibe.services · **For AI:** https://openvibe.services/llms.txt
**Install and test:** `npm ci && npm test` (Node 20 or newer)
**Install and test:** `npm ci && npm test` (Node 20 or newer)
-**Status:** alpha (first released in Wave 1; the release is package.json's `version`, tagged). At v0.408.0: 1238 schemas in `contracts/catalog.json`, 66 service manifests, 516 capability manifests, 14 namespace manifests and the staff capability map. Service repositories run `openvibe-contracts-check` and the pin-drift check in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Consumers pin release tags (each repository's `STATUS.json` names its pin). `node scripts/docs-currency.js <root>` checks every checkout's README and `STATUS.json` against its code: the pins and versions they name, a library's consumer list, the deployed flag against the manifest, README links, and the paths and migrations they name. `node scripts/estate.js` generates `docs/ESTATE.md` and `manifests/repositories/` from the estate checkouts, and `--check` fails on drift (plan T1 step 3).
+**Status:** alpha (first released in Wave 1; the release is package.json's `version`, tagged). At v0.410.0: 1261 schemas in `contracts/catalog.json`, 67 service manifests, 523 capability manifests, 14 namespace manifests and the staff capability map. Service repositories run `openvibe-contracts-check` and the pin-drift check in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Consumers pin release tags (each repository's `STATUS.json` names its pin). `node scripts/docs-currency.js <root>` checks every checkout's README and `STATUS.json` against its code: the pins and versions they name, a library's consumer list, the deployed flag against the manifest, README links, and the paths and migrations they name. `node scripts/estate.js` generates `docs/ESTATE.md` and `manifests/repositories/` from the estate checkouts, and `--check` fails on drift (plan T1 step 3).
**Plan:** OpenVibe Development Roadmap, Wave 1 (implementation plan rev 3, §3.1 and §18.11).
**Plan:** OpenVibe Development Roadmap, Wave 1 (implementation plan rev 3, §3.1 and §18.11).
**License:** AGPL-3.0 (same as every OpenVibe service).
**License:** AGPL-3.0 (same as every OpenVibe service).
new file mode 100644
new file mode 100644
@@ -0,0 +1,925 @@
@@ -0,0 +1,925 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ai/deep-dive.v1.json",
+ "title": "AiDeepDive",
+ "description": "A deep dive: expert design research run by OpenVibe.AI (workflow design.deep_dive) for OpenVibe.Exchange's /deep-dive page and, through the API, OpenVibe.Design. One document holds the question and depth (the input), the seven steps, every source read, every capture (a private Media object per viewport and theme, with its DOM facts and Expert extras), the per-site analysis, the synthesis across sites, the recommendations checked against OpenVibe's own rules, and the receipt of every model call and capture. Every claim carries its evidence: the site, the capture or crop, the URL and the date it was read. Nothing is copied: captures are private to the run's owner and the report shows them as attributed commentary.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "schema",
+ "run_id",
+ "status",
+ "input",
+ "steps",
+ "sources",
+ "captures",
+ "analysis",
+ "synthesis",
+ "recommendations",
+ "receipt",
+ "gaps",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The run id; a deep dive is a run (GET /api/v1/deep-dives/{id})."
+ },
+ "schema": {
+ "const": "ai.deep-dive@1"
+ },
+ "run_id": {
+ "type": "string",
+ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "status": {
+ "enum": [
+ "queued",
+ "running",
+ "choosing",
+ "succeeded",
+ "failed",
+ "cancelled"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "question",
+ "depth",
+ "max_sites"
+ ],
+ "properties": {
+ "question": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "depth": {
+ "enum": [
+ "quick",
+ "thorough",
+ "expert"
+ ]
+ },
+ "max_sites": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 30
+ },
+ "sites": {
+ "type": "array",
+ "maxItems": 30,
+ "items": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "description": "Sites the person named when starting the run."
+ }
+ }
+ },
+ "steps": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "key",
+ "status"
+ ],
+ "properties": {
+ "key": {
+ "enum": [
+ "plan",
+ "search",
+ "choose",
+ "capture",
+ "analyse",
+ "synthesise",
+ "report"
+ ]
+ },
+ "status": {
+ "enum": [
+ "pending",
+ "running",
+ "done",
+ "skipped",
+ "failed"
+ ]
+ },
+ "detail": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "finished_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "sources": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "kind",
+ "title",
+ "url",
+ "read_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "kind": {
+ "enum": [
+ "web",
+ "picture",
+ "icon",
+ "project",
+ "rules",
+ "component",
+ "named"
+ ]
+ },
+ "title": {
+ "type": "string",
+ "maxLength": 400
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "publisher": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200
+ },
+ "engine": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 80
+ },
+ "snippet": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "license": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 120
+ },
+ "read_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "captures": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "source_id",
+ "url",
+ "captured_at",
+ "viewport",
+ "theme",
+ "full_page",
+ "crops"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^cap_[A-Za-z0-9_-]{1,40}$"
+ },
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "final_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 400
+ },
+ "captured_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "viewport": {
+ "enum": [
+ 390,
+ 1280
+ ]
+ },
+ "theme": {
+ "enum": [
+ "light",
+ "dark"
+ ]
+ },
+ "full_page": {
+ "type": "boolean"
+ },
+ "media_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "width": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "height": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "crops": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "media_id",
+ "y",
+ "height"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^crop_[A-Za-z0-9_-]{1,40}$"
+ },
+ "media_id": {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "y": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "height": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ "dom": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "extras": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "skip": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200,
+ "description": "Why the capture did not happen: robots_disallowed, not_public, no_chrome, timeout, …"
+ },
+ "cached": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "analysis": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "source_id",
+ "url",
+ "summary",
+ "strengths",
+ "weaknesses",
+ "evidence"
+ ],
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "summary": {
+ "type": "string",
+ "maxLength": 4000
+ },
+ "first_impression": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "hierarchy": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "density": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "type": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "colour": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "motion": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "words": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "strengths": {
+ "type": "array",
+ "maxItems": 12,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ },
+ "weaknesses": {
+ "type": "array",
+ "maxItems": 12,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ },
+ "accessibility": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "speed": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ }
+ }
+ }
+ },
+ "synthesis": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "patterns",
+ "comparison",
+ "best",
+ "mistakes",
+ "gap",
+ "reference_tokens",
+ "do",
+ "dont",
+ "accessibility",
+ "speed"
+ ],
+ "properties": {
+ "patterns": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "what",
+ "sites",
+ "evidence"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "maxLength": 160
+ },
+ "what": {
+ "type": "string",
+ "maxLength": 1200
+ },
+ "sites": {
+ "type": "array",
+ "maxItems": 30,
+ "items": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ }
+ },
+ "usage_count": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 30
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ }
+ }
+ }
+ },
+ "comparison": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "columns",
+ "rows"
+ ],
+ "properties": {
+ "columns": {
+ "type": "array",
+ "maxItems": 12,
+ "items": {
+ "type": "string",
+ "maxLength": 120
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "maxItems": 12,
+ "items": {
+ "type": "string",
+ "maxLength": 1000
+ }
+ }
+ }
+ }
+ },
+ "best": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "source_id",
+ "what",
+ "evidence"
+ ],
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "what": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ }
+ }
+ },
+ "mistakes": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "what",
+ "evidence"
+ ],
+ "properties": {
+ "what": {
+ "type": "string",
+ "maxLength": 1200
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ }
+ }
+ }
+ },
+ "gap": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "reference_tokens": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "palettes",
+ "type_scales",
+ "spacing"
+ ],
+ "properties": {
+ "palettes": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "source_id",
+ "colours"
+ ],
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "colours": {
+ "type": "array",
+ "maxItems": 16,
+ "items": {
+ "type": "string",
+ "maxLength": 40
+ }
+ }
+ }
+ }
+ },
+ "type_scales": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "source_id",
+ "families",
+ "sizes"
+ ],
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "families": {
+ "type": "array",
+ "maxItems": 8,
+ "items": {
+ "type": "string",
+ "maxLength": 120
+ }
+ },
+ "sizes": {
+ "type": "array",
+ "maxItems": 16,
+ "items": {
+ "type": "string",
+ "maxLength": 40
+ }
+ }
+ }
+ }
+ },
+ "spacing": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "source_id",
+ "steps"
+ ],
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "steps": {
+ "type": "array",
+ "maxItems": 16,
+ "items": {
+ "type": "string",
+ "maxLength": 40
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "do": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ },
+ "dont": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ },
+ "accessibility": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ },
+ "speed": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ }
+ }
+ },
+ "recommendations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "what",
+ "why",
+ "rule",
+ "evidence"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^rec_[A-Za-z0-9_-]{1,40}$"
+ },
+ "what": {
+ "type": "string",
+ "maxLength": 1200
+ },
+ "why": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "rule": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "status",
+ "text"
+ ],
+ "properties": {
+ "id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 120
+ },
+ "status": {
+ "enum": [
+ "follows",
+ "would_change",
+ "none"
+ ]
+ },
+ "text": {
+ "type": "string",
+ "maxLength": 600
+ }
+ }
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ }
+ }
+ }
+ },
+ "receipt": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "price_usd",
+ "held_usd",
+ "cost_usd",
+ "models",
+ "captures_count",
+ "synthetic"
+ ],
+ "properties": {
+ "price_usd": {
+ "type": "number",
+ "minimum": 0
+ },
+ "held_usd": {
+ "type": "number",
+ "minimum": 0
+ },
+ "cost_usd": {
+ "type": "number",
+ "minimum": 0
+ },
+ "captures_count": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "synthetic": {
+ "type": "boolean"
+ },
+ "models": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "step",
+ "provider",
+ "model",
+ "tokens_in",
+ "tokens_out",
+ "cost_usd",
+ "ms"
+ ],
+ "properties": {
+ "step": {
+ "enum": [
+ "plan",
+ "choose",
+ "analyse",
+ "synthesise"
+ ]
+ },
+ "provider": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 80
+ },
+ "model": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200
+ },
+ "tokens_in": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "tokens_out": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "cost_usd": {
+ "type": "number",
+ "minimum": 0
+ },
+ "ms": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ }
+ }
+ },
+ "gaps": {
+ "type": "array",
+ "maxItems": 40,
+ "items": {
+ "type": "string",
+ "maxLength": 400
+ }
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "finished_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ },
+ "$defs": {
+ "evidence": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "claim",
+ "url",
+ "read_at"
+ ],
+ "properties": {
+ "claim": {
+ "type": "string",
+ "maxLength": 600
+ },
+ "source_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "capture_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^cap_[A-Za-z0-9_-]{1,40}$"
+ },
+ "crop_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^crop_[A-Za-z0-9_-]{1,40}$"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "read_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ }
+ }
+}
@@ -11573,6 +11573,125 @@
@@ -11573,6 +11573,125 @@
"visibility": "first-party",
"visibility": "first-party",
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"status": "active"
+ },
+ {
+ "id": "ui.surface",
+ "version": "1.0.0",
+ "owner": "contracts",
+ "schema": "ui/surface.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "ui.layout",
+ "version": "1.0.0",
+ "owner": "contracts",
+ "schema": "ui/layout.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "ui.layout-request",
+ "version": "1.0.0",
+ "owner": "contracts",
+ "schema": "ui/layout-request.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "ui.layout-suggestion",
+ "version": "1.0.0",
+ "owner": "contracts",
+ "schema": "ui/layout-suggestion.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "network.layout.changed",
+ "version": "1.0.0",
+ "owner": "network",
+ "schema": "events/payloads/network.layout.changed.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "ui.action",
+ "version": "1.0.0",
+ "owner": "contracts",
+ "schema": "ui/action.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "ui.element",
+ "version": "1.0.0",
+ "owner": "contracts",
+ "schema": "ui/element.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "ui.element-state",
+ "version": "1.0.0",
+ "owner": "contracts",
+ "schema": "ui/element-state.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "ui.trigger",
+ "version": "1.0.0",
+ "owner": "contracts",
+ "schema": "ui/trigger.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "live.element.pressed",
+ "version": "1.0.0",
+ "owner": "live",
+ "schema": "events/payloads/live.element.pressed.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "live.element.state",
+ "version": "1.0.0",
+ "owner": "live",
+ "schema": "events/payloads/live.element.state.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "ai.deep-dive",
+ "version": "1.0.0",
+ "owner": "ai",
+ "schema": "ai/deep-dive.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active",
+ "adr": "ADR-015"
+ },
+ {
+ "id": "ai.deep_dive.updated",
+ "version": "1.0.0",
+ "owner": "ai",
+ "schema": "events/payloads/ai.deep_dive.updated.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active",
+ "adr": "ADR-015"
}
}
]
]
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,147 @@
@@ -0,0 +1,147 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/ai.deep_dive.updated.v1.json",
+ "title": "AiDeepDiveUpdatedPayload",
+ "description": "ai.deep_dive.updated v1 (OpenVibe.AI server/deep-dive): a deep dive's step changed or it ended. One event per step transition and one at the end, so a page (OpenVibe.Exchange's /deep-dive) can follow a run without polling, and OpenVibe.Design can attach a finished report. The payload carries the run and deep-dive ids, the step, the progress and the status; never the question's answer, a capture or a prompt. Envelope: subject { type: run, id: <run_id> }, visibility internal, priority low (important on failed), actor service:ai.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "deep_dive_id",
+ "run_id",
+ "status",
+ "step",
+ "progress",
+ "steps",
+ "depth",
+ "requester",
+ "on_behalf_of",
+ "url",
+ "error",
+ "occurred_at"
+ ],
+ "properties": {
+ "deep_dive_id": {
+ "type": "string",
+ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "run_id": {
+ "type": "string",
+ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "status": {
+ "enum": [
+ "queued",
+ "running",
+ "choosing",
+ "succeeded",
+ "failed",
+ "cancelled"
+ ]
+ },
+ "step": {
+ "enum": [
+ "plan",
+ "search",
+ "choose",
+ "capture",
+ "analyse",
+ "synthesise",
+ "report",
+ "done",
+ null
+ ]
+ },
+ "progress": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "steps": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "key",
+ "status"
+ ],
+ "properties": {
+ "key": {
+ "enum": [
+ "plan",
+ "search",
+ "choose",
+ "capture",
+ "analyse",
+ "synthesise",
+ "report"
+ ]
+ },
+ "status": {
+ "enum": [
+ "pending",
+ "running",
+ "done",
+ "skipped",
+ "failed"
+ ]
+ }
+ }
+ }
+ },
+ "depth": {
+ "enum": [
+ "quick",
+ "thorough",
+ "expert"
+ ]
+ },
+ "requester": {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ "on_behalf_of": {
+ "anyOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://"
+ },
+ "error": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "code",
+ "detail"
+ ],
+ "properties": {
+ "code": {
+ "type": "string",
+ "maxLength": 80
+ },
+ "detail": {
+ "type": "string",
+ "maxLength": 500
+ }
+ }
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "occurred_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/live.element.pressed.v1.json",
+ "title": "LiveElementPressedPayload",
+ "description": "live.element.pressed v1 (OpenVibe.Live server/elements): a viewer's element press was accepted and ran. It carries the public facts the stream should see: the channel, the element's id and name, the presser's public name and subject, the trigger that fired, the channel points it cost and the element-state revision it left behind. It never carries the presser's email, address, account balance or anything private; a refused press does not produce it. Envelope: subject { type: element, id: el_… }, actor the presser's subject, visibility public, priority low. Also written to the channel's AI timeline as a moment, so the stream's own timeline can cite 'chat made the robot dance'.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["channel", "element_id", "element_name", "presser", "presser_name", "trigger", "at"],
+ "properties": {
+ "channel": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["username", "display_name"],
+ "properties": {
+ "username": { "type": "string", "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$" },
+ "display_name": { "type": "string", "minLength": 1, "maxLength": 80 }
+ },
+ "description": "The public channel facts (the channel page is public already)."
+ },
+ "element_id": { "type": "string", "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "element_name": { "type": "string", "minLength": 1, "maxLength": 80 },
+ "presser": { "$ref": "../../identity/subject-ref.v1.json" },
+ "presser_name": { "type": "string", "minLength": 1, "maxLength": 120, "description": "The display name the stream shows, never an email or an account id." },
+ "trigger": { "$ref": "../../ui/trigger.v1.json" },
+ "points_spent": { "type": "integer", "minimum": 0, "description": "Channel points the press cost; 0 when it was free." },
+ "state_revision": { "type": "integer", "minimum": 0, "description": "The element-state revision after the actions ran; 0 when the element has no state yet." },
+ "at": { "type": "string", "format": "date-time" }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/live.element.state.v1.json",
+ "title": "LiveElementStatePayload",
+ "description": "live.element.state v1 (OpenVibe.Live server/elements): an element's own store changed, broadcast to everyone watching through Live's existing events path. Carries the channel's public facts and the same store as ui.element-state@1: the element instance, a revision that grows by one per accepted change, the values themselves and when. It is public state (a counter, a score, a wheel's entries), never the presser's data: a value may not be an object, a markup string or a URL, and the payload carries no account.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "channel",
+ "element_id",
+ "revision",
+ "values",
+ "updated_at"
+ ],
+ "properties": {
+ "channel": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "username",
+ "display_name"
+ ],
+ "properties": {
+ "username": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$"
+ },
+ "display_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ }
+ }
+ },
+ "element_id": {
+ "type": "string",
+ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "values": {
+ "$ref": "../../ui/element-state.v1.json#/$defs/values"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/network.layout.changed.v1.json",
+ "title": "NetworkLayoutChangedPayload",
+ "description": "network.layout.changed v1 (OpenVibe.Network server/layouts, written into network_event_outbox in the transaction that changes a stored layout). One event per change to one person's ui.layout@1: set active, saved, renamed, deleted, shared, unshared, published to the gallery, unpublished, copied from the gallery, or reset to the default. Envelope: subject { type: user, id } and the layout's revision, actor the person, visibility internal. The payload carries no account data and no layout contents, so an open page can announce a change and refetch with the person's own token.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["surface", "device", "layout_id", "change", "revision"],
+ "properties": {
+ "surface": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",
+ "maxLength": 80
+ },
+ "device": { "enum": ["phone", "tablet", "computer"] },
+ "layout_id": { "type": "string", "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "change": {
+ "enum": ["active", "saved", "renamed", "deleted", "shared", "unshared", "published", "unpublished", "copied", "reset"]
+ },
+ "revision": { "type": "integer", "minimum": 1 },
+ "sharing": { "enum": ["private", "link", "gallery"] }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,702 @@
@@ -0,0 +1,702 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/action.v1.json",
+ "title": "UiAction",
+ "description": "ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop, the e-stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.",
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "direction",
+ "ms"
+ ],
+ "description": "Drive the robot for a moment, then stop. The duration is bounded by Bot's own max_command_ms and the owner's limits for the presser's role.",
+ "properties": {
+ "kind": {
+ "const": "robot.drive_pulse"
+ },
+ "product": {
+ "const": "bot"
+ },
+ "direction": {
+ "enum": [
+ "forward",
+ "back",
+ "left",
+ "right",
+ "spin_left",
+ "spin_right"
+ ]
+ },
+ "ms": {
+ "type": "integer",
+ "minimum": 50,
+ "maximum": 3000
+ },
+ "speed": {
+ "type": "number",
+ "minimum": 0.1,
+ "maximum": 1,
+ "description": "Fraction of the robot's allowed speed; omitted means the owner's default for the role."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "preset"
+ ],
+ "description": "Take the robot's head to a preset. Bot maps up, center and down to the head actuator its robot profile declares; a robot without one refuses it.",
+ "properties": {
+ "kind": {
+ "const": "robot.head_preset"
+ },
+ "product": {
+ "const": "bot"
+ },
+ "preset": {
+ "enum": [
+ "up",
+ "center",
+ "down"
+ ],
+ "description": "A head tilt preset. Bot still checks the robot profile and the owner's limits."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "on"
+ ],
+ "description": "Turn the robot's headlight on or off. Bot's own headlight kind is the authority: only the owner and People Operators may send it, and a robot whose profile has no headlight refuses it.",
+ "properties": {
+ "kind": {
+ "const": "robot.headlight"
+ },
+ "product": {
+ "const": "bot"
+ },
+ "on": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "text"
+ ],
+ "description": "Speak a line through the robot. Text goes through the existing content checks and Bot's own speak limits.",
+ "properties": {
+ "kind": {
+ "const": "robot.say"
+ },
+ "product": {
+ "const": "bot"
+ },
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "macro"
+ ],
+ "description": "Replay a recorded robot macro through the safe command path, with its own deadline. Macros are stage 4 of Elements; a stage-1 product refuses this kind as not yet supported.",
+ "properties": {
+ "kind": {
+ "const": "robot.macro"
+ },
+ "product": {
+ "const": "bot"
+ },
+ "macro": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "text"
+ ],
+ "description": "Send a chat message as the channel's bot. Text goes through Chat's content checks and rate limits.",
+ "properties": {
+ "kind": {
+ "const": "chat.message"
+ },
+ "product": {
+ "const": "chat"
+ },
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 400
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "source"
+ ],
+ "properties": {
+ "kind": {
+ "const": "chat.pin"
+ },
+ "product": {
+ "const": "chat"
+ },
+ "source": {
+ "enum": [
+ "trigger",
+ "latest"
+ ],
+ "description": "trigger pins the message that caused this action; latest pins the newest chat message."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "question",
+ "options"
+ ],
+ "properties": {
+ "kind": {
+ "const": "chat.poll"
+ },
+ "product": {
+ "const": "chat"
+ },
+ "question": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "options": {
+ "type": "array",
+ "minItems": 2,
+ "maxItems": 10,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "title"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.alert"
+ },
+ "product": {
+ "const": "live"
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "name"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.animation"
+ },
+ "product": {
+ "const": "live"
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "An animation the page or overlay has; an unknown name is refused, never fetched."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.confetti"
+ },
+ "product": {
+ "const": "live"
+ },
+ "intensity": {
+ "enum": [
+ "low",
+ "medium",
+ "high"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "sound"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.sound"
+ },
+ "product": {
+ "const": "live"
+ },
+ "sound": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "A sound from the streamer's own sound list; Live never fetches a URL for it."
+ },
+ "volume": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "text",
+ "voice"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.tts"
+ },
+ "product": {
+ "const": "live"
+ },
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ },
+ "voice": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "A voice from the streamer's own voice list."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "key",
+ "by"
+ ],
+ "description": "Move one of the element's counters. The host applies it to the element's own state and Live broadcasts the new state.",
+ "properties": {
+ "kind": {
+ "const": "screen.counter"
+ },
+ "product": {
+ "const": "live"
+ },
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{0,31}$"
+ },
+ "by": {
+ "type": "integer",
+ "minimum": -1000,
+ "maximum": 1000
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "key",
+ "value"
+ ],
+ "description": "Set one of the element's state values: a boolean, a number, a short string or a list of those (a wheel's entries, a scoreboard). Objects are not state values.",
+ "properties": {
+ "kind": {
+ "const": "screen.state"
+ },
+ "product": {
+ "const": "live"
+ },
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{0,31}$"
+ },
+ "value": {
+ "oneOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "maxLength": 200
+ },
+ {
+ "type": "array",
+ "maxItems": 64,
+ "items": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "maxLength": 200
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "element",
+ "visible"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.visibility"
+ },
+ "product": {
+ "const": "live"
+ },
+ "element": {
+ "type": "string",
+ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "scene"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.scene"
+ },
+ "product": {
+ "const": "live"
+ },
+ "scene": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "A scene the layout has; an unknown scene is refused."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product"
+ ],
+ "properties": {
+ "kind": {
+ "const": "stream.clip"
+ },
+ "product": {
+ "const": "live"
+ },
+ "seconds": {
+ "type": "integer",
+ "minimum": 5,
+ "maximum": 30,
+ "description": "How far back the clip starts, at most 30 seconds."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product"
+ ],
+ "properties": {
+ "kind": {
+ "const": "stream.moment"
+ },
+ "product": {
+ "const": "live"
+ },
+ "label": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "title"
+ ],
+ "properties": {
+ "kind": {
+ "const": "stream.title"
+ },
+ "product": {
+ "const": "live"
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 140
+ }
+ },
+ "description": "Change the stream title. Live allows it only for the channel's owner, whatever the element's trigger says."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "category"
+ ],
+ "properties": {
+ "kind": {
+ "const": "stream.category"
+ },
+ "product": {
+ "const": "live"
+ },
+ "category": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ }
+ },
+ "description": "Change the stream category. Live allows it only for the channel's owner."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "prompt"
+ ],
+ "description": "Ask an AI model through Exchange's catalogue, with the element's budget and a receipt. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",
+ "properties": {
+ "kind": {
+ "const": "ai.ask"
+ },
+ "product": {
+ "const": "ai"
+ },
+ "prompt": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "model": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "max_cost_usd": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "task"
+ ],
+ "description": "Hand a task to an Actor agent. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",
+ "properties": {
+ "kind": {
+ "const": "actor.task"
+ },
+ "product": {
+ "const": "actor"
+ },
+ "task": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 8000
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "endpoint"
+ ],
+ "description": "Call one of the streamer's own webhook endpoints. The element names the endpoint the streamer listed; it never carries a URL, and the payload never carries a viewer's data. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",
+ "properties": {
+ "kind": {
+ "const": "webhook.call"
+ },
+ "product": {
+ "const": "live"
+ },
+ "endpoint": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_-]{0,31}$"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "goal"
+ ],
+ "description": "Update a tip goal's display. Money never moves here; Billing's own sheet confirms any charge. Stage 4 of Elements.",
+ "properties": {
+ "kind": {
+ "const": "money.tip_goal"
+ },
+ "product": {
+ "const": "billing"
+ },
+ "goal": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40
+ }
+ }
+ }
+ ],
+ "$defs": {
+ "capability": {
+ "description": "The action kinds an Element may declare in ui.element@1 capabilities. One id, one product: the host routes an action only to the product that owns it.",
+ "enum": [
+ "robot.drive_pulse",
+ "robot.head_preset",
+ "robot.headlight",
+ "robot.say",
+ "robot.macro",
+ "chat.message",
+ "chat.pin",
+ "chat.poll",
+ "screen.alert",
+ "screen.animation",
+ "screen.confetti",
+ "screen.sound",
+ "screen.tts",
+ "screen.counter",
+ "screen.state",
+ "screen.visibility",
+ "screen.scene",
+ "stream.clip",
+ "stream.moment",
+ "stream.title",
+ "stream.category",
+ "ai.ask",
+ "actor.task",
+ "webhook.call",
+ "money.tip_goal"
+ ]
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/element-state.v1.json",
+ "title": "UiElementState",
+ "description": "ui.element-state@1: the small store an Element keeps for itself (counters, scores, lists, a wheel's entries), as Live broadcasts it to everyone watching and as a read returns it. CHANNEL and ELEMENT_ID name the element instance, REVISION grows by one with each accepted change so a late frame never overwrites a newer one, and VALUES is the store itself: at most 32 keys, each a boolean, a number, a short string or a list of those. It is state, not code: no objects, no markup, no URL and no account data, which keeps a shared element's state safe to render for every viewer.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["channel", "element_id", "revision", "values", "updated_at"],
+ "properties": {
+ "channel": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$",
+ "description": "The Live channel's username (its slug)."
+ },
+ "element_id": {
+ "type": "string",
+ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "values": { "$ref": "#/$defs/values" },
+ "updated_at": { "type": "string", "format": "date-time" }
+ },
+ "$defs": {
+ "values": {
+ "type": "object",
+ "maxProperties": 32,
+ "propertyNames": { "pattern": "^[a-z][a-z0-9_]{0,31}$" },
+ "additionalProperties": { "$ref": "#/$defs/value" },
+ "description": "The element's own store. A key is lower-case and 32 characters at most; a value is a boolean, a number, a string of 200 characters at most or a list of those."
+ },
+ "value": {
+ "oneOf": [
+ { "type": "boolean" },
+ { "type": "number" },
+ { "type": "string", "maxLength": 200 },
+ {
+ "type": "array",
+ "maxItems": 64,
+ "items": {
+ "oneOf": [
+ { "type": "number" },
+ { "type": "string", "maxLength": 200 }
+ ]
+ }
+ }
+ ]
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,203 @@
@@ -0,0 +1,203 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/element.v1.json",
+ "title": "UiElement",
+ "description": "ui.element@1: an Element people make and place on a surface: a look, a behaviour and, only when a recipe is not enough, a mod. LOOK draws from OpenVibe.Design's components and the theme's tokens, so it follows the person's theme, the seasons and dark/light; it carries no markup and no URL. RECIPE is data only: one or more triggers from ui.trigger@1, the conditions that decide who may fire it and what it costs, and one or more actions from ui.action@1, checked against CAPABILITIES. A capability is an action kind the element declares; the host refuses an action the element did not declare, and the product named by the action still checks the presser's role, the cooldown, the cost and its own limits. MOD is an optional reference to a code module (id, version and digest) for the stage where a recipe is not enough; the module never adds a power. Conditions use channel points only in stage 1: no Vibes, no money, and Billing still confirms any charge itself. The id and revision are assigned by the store; a draft has neither.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name", "author", "sharing", "look", "recipe", "capabilities"],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Assigned by the store when the element is saved; absent in a draft."
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Assigned by the store; every saved change moves it by one."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 500
+ },
+ "author": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name"],
+ "properties": {
+ "name": { "type": "string", "minLength": 1, "maxLength": 80 },
+ "subject": { "$ref": "../identity/subject-ref.v1.json" }
+ },
+ "description": "The author's display name and, for a private or link element, the subject. A gallery or copy answer carries the display name only, never the account."
+ },
+ "sharing": { "enum": ["private", "link", "gallery"] },
+ "look": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["component", "label"],
+ "properties": {
+ "component": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,63}$",
+ "description": "An OpenVibe.Design component name (design.component@1), e.g. button or meter. A name the page does not have is refused, never fetched."
+ },
+ "label": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "The words on the element, e.g. Make it dance."
+ },
+ "icon": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,31}$",
+ "description": "A glyph name from the design icon set."
+ },
+ "props": {
+ "type": "object",
+ "maxProperties": 20,
+ "propertyNames": { "pattern": "^[a-z][a-zA-Z0-9]{0,31}$" },
+ "additionalProperties": {
+ "oneOf": [
+ { "type": "boolean" },
+ { "type": "number" },
+ { "type": "string", "maxLength": 200 }
+ ]
+ },
+ "description": "The design component's own props (the sliders the editor shows). Numbers, strings and booleans only; no nesting."
+ },
+ "style": { "$ref": "#/$defs/style" }
+ }
+ },
+ "recipe": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["triggers", "actions"],
+ "properties": {
+ "triggers": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 8,
+ "items": { "$ref": "trigger.v1.json" }
+ },
+ "conditions": { "$ref": "#/$defs/conditions" },
+ "actions": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 16,
+ "items": { "$ref": "action.v1.json" }
+ }
+ }
+ },
+ "capabilities": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": { "$ref": "action.v1.json#/$defs/capability" },
+ "description": "The action kinds this element may request. Every action in the recipe must be declared here; the host refuses the rest."
+ },
+ "initial_state": {
+ "$ref": "element-state.v1.json#/$defs/values",
+ "description": "The element's state before anything runs; the counter starts at 0, never at a value the element did not declare."
+ },
+ "mod": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "version", "sha256"],
+ "properties": {
+ "id": { "type": "string", "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$"
+ },
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
+ "capabilities": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": { "$ref": "action.v1.json#/$defs/capability" },
+ "description": "The subset of the element's capabilities the module itself may request; omitted means the element's declared capabilities, and a product may grant fewer."
+ }
+ },
+ "description": "A code module for an element whose recipe is not enough (stage 3). It runs sandboxed; the reference is data, and the host checks every request the module makes against the element's capabilities."
+ },
+ "created_at": { "type": "string", "format": "date-time" },
+ "updated_at": { "type": "string", "format": "date-time" }
+ },
+ "$defs": {
+ "style": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "tint": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,39}$",
+ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."
+ },
+ "shape": { "enum": ["round", "pill", "square"] },
+ "opacity": { "type": "number", "minimum": 0.4, "maximum": 1 },
+ "show_label": { "type": "boolean" }
+ }
+ },
+ "conditions": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "role": {
+ "enum": ["everyone", "follower", "subscriber", "vip", "operator", "owner"],
+ "description": "Who may fire it. A higher role admits the lower ones where the product says so (an operator is admitted to subscriber-only); owner is the channel's owner alone. Default everyone."
+ },
+ "cost": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["points"],
+ "properties": {
+ "points": { "type": "integer", "minimum": 1, "maximum": 1000000 }
+ },
+ "description": "Channel points the presser spends, from Live's points. Stage 1 has no other currency, and an element never charges money."
+ },
+ "cooldown": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "per_person_seconds": { "type": "integer", "minimum": 0, "maximum": 86400 },
+ "overall_seconds": { "type": "integer", "minimum": 0, "maximum": 86400 }
+ },
+ "description": "How long the same person must wait, and how long everybody must wait, before it can fire again."
+ },
+ "daily_cap": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "per_person": { "type": "integer", "minimum": 1, "maximum": 10000 },
+ "overall": { "type": "integer", "minimum": 1, "maximum": 1000000 }
+ },
+ "description": "The most times it may fire in a UTC day, per person and for everyone."
+ },
+ "live_only": {
+ "type": "boolean",
+ "description": "When true the element fires only while the channel is live."
+ },
+ "robot_turn_only": {
+ "type": "boolean",
+ "description": "When true a robot action is requested only while the presser has the robot's turn; Bot still decides."
+ },
+ "chance": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["one_in"],
+ "properties": {
+ "one_in": { "type": "integer", "minimum": 1, "maximum": 1000000 }
+ },
+ "description": "Fire only one time in N. The host draws the lot, so a refused draw costs nothing."
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/layout-request.v1.json",
+ "title": "UiLayoutRequest",
+ "description": "ui.layout-request@1: what an editor sends OpenVibe.AI's ui.layout.suggest task, and what Network forwards for a signed-in person. It is the surface's catalog, the layout shown now (or null for the default one) and the words the person typed (max 500 characters), for one device class. The catalog is the only power the answer can use; the current layout is the starting arrangement; the words never contain markup that becomes part of it.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["catalog", "layout", "words", "device"],
+ "properties": {
+ "catalog": { "$ref": "surface.v1.json" },
+ "layout": {
+ "oneOf": [
+ { "$ref": "layout.v1.json" },
+ { "type": "null" }
+ ],
+ "description": "The arrangement shown now, or null when the person is looking at the default."
+ },
+ "words": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500,
+ "description": "The person's own description, e.g. \"put Stop on the left and make it huge\"."
+ },
+ "device": { "enum": ["phone", "tablet", "computer"] }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/layout-suggestion.v1.json",
+ "title": "UiLayoutSuggestion",
+ "description": "ui.layout-suggestion@1: OpenVibe.AI's answer to ui.layout.suggest. ok true carries the layout, already checked by openvibe-sdk/ui-layout against the catalog sent with the request, and a one-sentence summary of what changed plus the model's reasons. ok false is an honest refusal: no layout, one sentence saying what it could not do, and the validator's reasons in plain words (for example \"Stop has to stay visible.\"). A receipt names the model that answered, with the run and the route. The suggestion is previewed, never applied silently; Network or the product stores it only after the person accepts it.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["ok", "layout", "summary", "reasons"],
+ "properties": {
+ "ok": { "type": "boolean" },
+ "layout": {
+ "oneOf": [
+ { "$ref": "layout.v1.json" },
+ { "type": "null" }
+ ],
+ "description": "The validated layout when ok is true; null when it is false."
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300,
+ "description": "One sentence saying what changed, or what could not be done."
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "items": { "type": "string", "minLength": 1, "maxLength": 300 },
+ "description": "Why this arrangement, or the validator's reasons for refusing one."
+ },
+ "receipt": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["model"],
+ "properties": {
+ "model": { "type": "string", "minLength": 1, "maxLength": 200 },
+ "provider": { "type": "string", "minLength": 1, "maxLength": 100 },
+ "route": { "type": "string", "minLength": 1, "maxLength": 200 },
+ "run_id": { "type": "string", "minLength": 1, "maxLength": 80 },
+ "cost_usd": { "type": "number", "minimum": 0 }
+ },
+ "description": "Which model answered: the same receipt every AI run carries, at the task's own shape."
+ }
+ },
+ "allOf": [
+ {
+ "if": { "properties": { "ok": { "const": true } }, "required": ["ok"] },
+ "then": { "properties": { "layout": { "type": "object" } } }
+ },
+ {
+ "if": { "properties": { "ok": { "const": false } }, "required": ["ok"] },
+ "then": { "properties": { "layout": { "type": "null" } } }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,256 @@
@@ -0,0 +1,256 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/layout.v1.json",
+ "title": "UiLayout",
+ "description": "ui.layout@1: one person's arrangement of one surface on one device class. A layout names the surface and its catalog version, the device, its own name, its author and its sharing (private, link or gallery), and one entry per part it places. A part is visible or not, sits on the surface's grid (x, y, w, h in cells) or docks to an edge for an overlay, and carries a style (a theme token tint, a shape, opacity 0.4-1 and the label) and bindings. It is data only: no URL, no markup and no code may appear anywhere in it, and every part id, tint, shape, opacity, size and binding must be one the catalog allows. openvibe-sdk/ui-layout validates it before any server, editor or AI answer stores or shows it.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "surface",
+ "surface_version",
+ "device",
+ "name",
+ "author",
+ "sharing",
+ "parts"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Assigned by OpenVibe.Network when the layout is stored; absent in a draft, a share answer or an AI suggestion."
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Assigned by the store; every change to a stored layout moves it by one."
+ },
+ "active": {
+ "type": "boolean",
+ "description": "Set by OpenVibe.Network: the one layout a product draws for this surface on this device class. A draft, a share answer and an AI suggestion carry no active field."
+ },
+ "surface": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",
+ "maxLength": 80
+ },
+ "surface_version": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "device": {
+ "enum": [
+ "phone",
+ "tablet",
+ "computer"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "The person's name for this layout; \"My phone controls\" is fine."
+ },
+ "author": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "subject": {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ },
+ "description": "The author's display name and, for a private or link layout, the subject. A share or gallery answer carries the display name only, never the account."
+ },
+ "sharing": {
+ "enum": [
+ "private",
+ "link",
+ "gallery"
+ ]
+ },
+ "parts": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 64,
+ "items": {
+ "$ref": "#/$defs/part"
+ }
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "$defs": {
+ "place": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "x",
+ "y",
+ "w",
+ "h"
+ ],
+ "properties": {
+ "x": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 23
+ },
+ "y": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100
+ },
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ }
+ },
+ "description": "The part's cell on the surface grid."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "dock",
+ "order",
+ "w",
+ "h"
+ ],
+ "properties": {
+ "dock": {
+ "enum": [
+ "left",
+ "right",
+ "top",
+ "bottom"
+ ]
+ },
+ "order": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 63
+ },
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ }
+ },
+ "description": "For an overlay: the edge the part docks to, its order along that edge, and its size in cells."
+ }
+ ]
+ },
+ "style": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "tint",
+ "shape",
+ "opacity",
+ "label"
+ ],
+ "properties": {
+ "tint": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,39}$",
+ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."
+ },
+ "shape": {
+ "enum": [
+ "round",
+ "pill",
+ "square"
+ ]
+ },
+ "opacity": {
+ "type": "number",
+ "minimum": 0.4,
+ "maximum": 1
+ },
+ "label": {
+ "type": "boolean"
+ }
+ }
+ },
+ "bindings": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "keys",
+ "gamepad"
+ ],
+ "properties": {
+ "keys": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"
+ }
+ },
+ "gamepad": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ }
+ }
+ }
+ },
+ "part": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "visible",
+ "place",
+ "style",
+ "bindings"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ },
+ "visible": {
+ "type": "boolean"
+ },
+ "place": {
+ "$ref": "#/$defs/place"
+ },
+ "style": {
+ "$ref": "#/$defs/style"
+ },
+ "bindings": {
+ "$ref": "#/$defs/bindings"
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,200 @@
@@ -0,0 +1,200 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/surface.v1.json",
+ "title": "UiSurface",
+ "description": "ui.surface@1: the catalog a product publishes for one surface people may arrange (bot.controls, bot.overlay, live.channel, dock, chat.composer, navbar.shortcuts). It names the surface, its device classes and grid, the theme tokens its styles draw from, and every part a layout may place. A part says what it is (stick, button, slider, readout, video, list), whether it is required or pressed, its minimum cells and the minimum pixels the product guarantees (44 px for anything pressed, larger for Stop), the sizes and styles it allows, the bindings it accepts, and the parts it must never cover. A layout is validated against this catalog (openvibe-sdk/ui-layout); a layout can arrange only what the catalog has and can never add a power. The catalog is data: a layout or an AI answer never carries code or a URL.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "version", "name", "devices", "grid", "theme_tokens", "parts"],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",
+ "maxLength": 80,
+ "description": "The surface id: <product>.<surface>, e.g. bot.controls."
+ },
+ "version": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The catalog version. A layout names the version it was made against."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "What the surface is called where a person sees it."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 500
+ },
+ "devices": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 3,
+ "uniqueItems": true,
+ "items": { "enum": ["phone", "tablet", "computer"] },
+ "description": "The device classes this surface offers."
+ },
+ "grid": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["columns", "cell_px"],
+ "properties": {
+ "columns": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["phone", "tablet", "computer"],
+ "description": "The grid's columns per device class: 4 on a phone and 12 on a computer in the design; a product may choose otherwise within these bounds.",
+ "properties": {
+ "phone": { "type": "integer", "minimum": 2, "maximum": 8 },
+ "tablet": { "type": "integer", "minimum": 2, "maximum": 16 },
+ "computer": { "type": "integer", "minimum": 2, "maximum": 24 }
+ }
+ },
+ "cell_px": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["phone", "tablet", "computer"],
+ "description": "The smallest a cell is rendered at that device class, in CSS pixels, at the narrowest supported width. The validator multiplies a part's cells by this to check the 44 px rule.",
+ "properties": {
+ "phone": { "type": "integer", "minimum": 24, "maximum": 200 },
+ "tablet": { "type": "integer", "minimum": 24, "maximum": 200 },
+ "computer": { "type": "integer", "minimum": 24, "maximum": 200 }
+ }
+ }
+ }
+ },
+ "theme_tokens": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,39}$" },
+ "description": "The theme tokens a tint may name (accent, surface, danger, ...). Never a colour value: the theme decides what the token is."
+ },
+ "parts": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 64,
+ "items": { "$ref": "#/$defs/part" }
+ }
+ },
+ "$defs": {
+ "part": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "name", "kind", "required", "pressed", "min", "styles", "bindings"],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$",
+ "description": "Stable part id inside this catalog, e.g. stop, stick, battery."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "kind": { "enum": ["stick", "button", "slider", "readout", "video", "list"] },
+ "required": {
+ "type": "boolean",
+ "description": "A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused."
+ },
+ "pressed": {
+ "type": "boolean",
+ "description": "The part sends a command when pressed or moved; it must render at least 44 px on every side."
+ },
+ "min": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["w", "h"],
+ "description": "The smallest size a layout may give the part: w and h in grid cells, and px when the product guarantees more than 44 px (Stop).",
+ "properties": {
+ "w": { "type": "integer", "minimum": 1, "maximum": 24 },
+ "h": { "type": "integer", "minimum": 1, "maximum": 24 },
+ "px": { "type": "integer", "minimum": 1, "maximum": 400 }
+ }
+ },
+ "sizes": {
+ "type": "array",
+ "maxItems": 12,
+ "uniqueItems": true,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["w", "h"],
+ "properties": {
+ "w": { "type": "integer", "minimum": 1, "maximum": 24 },
+ "h": { "type": "integer", "minimum": 1, "maximum": 24 }
+ }
+ },
+ "description": "Sizes the editor may offer as presets. A layout is not limited to these: it is limited to min and to the grid."
+ },
+ "styles": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["tints", "shapes"],
+ "properties": {
+ "tints": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 16,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,39}$" },
+ "description": "The theme tokens this part may be tinted with; every one is also in the surface's theme_tokens."
+ },
+ "shapes": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 3,
+ "uniqueItems": true,
+ "items": { "enum": ["round", "pill", "square"] }
+ },
+ "opacity": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["min", "max"],
+ "properties": {
+ "min": { "type": "number", "minimum": 0.4, "maximum": 1 },
+ "max": { "type": "number", "minimum": 0.4, "maximum": 1 }
+ },
+ "description": "The opacity range this part allows; the design's range is 0.4 to 1."
+ },
+ "label": {
+ "type": "boolean",
+ "description": "Whether the editor may show the part's label on it."
+ }
+ }
+ },
+ "bindings": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["keys", "gamepad"],
+ "properties": {
+ "keys": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[A-Za-z0-9+_.-]{1,40}$" }
+ },
+ "gamepad": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]{0,39}$" }
+ }
+ },
+ "description": "The bindings the product accepts for this part. A layout may pick a subset, never add one."
+ },
+ "never_covers": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]{0,39}$" },
+ "description": "The parts this part must never cover, in addition to the surface-wide rule that nothing covers a required part."
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,202 @@
@@ -0,0 +1,202 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/trigger.v1.json",
+ "title": "UiTrigger",
+ "description": "ui.trigger@1: one trigger an Element's recipe listens for, from the fixed catalog. kind says what happened: a viewer pressing, holding or double-pressing the element, a key or gamepad binding, a chat command or text, a follow, a tip, an AI Moment, a Rundown topic, a timer or schedule, robot telemetry or an event, the stream starting or ending, a viewer-count crossing or another element's event. The fields a kind needs are the only fields it accepts. A trigger is data: it never names code or a URL, and a product may refuse a kind it does not offer yet. Stage 1 of Elements uses press and chat_command; the rest are declared here so a stage-2 element and the editor speak one language.",
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind"],
+ "properties": {
+ "kind": { "const": "press" }
+ },
+ "description": "The element itself was pressed. This is the Viewer Deck's button."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "ms"],
+ "properties": {
+ "kind": { "const": "hold" },
+ "ms": { "type": "integer", "minimum": 300, "maximum": 10000 }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind"],
+ "properties": {
+ "kind": { "const": "double_press" },
+ "within_ms": { "type": "integer", "minimum": 100, "maximum": 2000, "description": "The longest gap between the two presses; 400 by default." }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "key"],
+ "properties": {
+ "kind": { "const": "key" },
+ "key": { "type": "string", "pattern": "^[A-Za-z0-9+_.-]{1,40}$" },
+ "modifiers": {
+ "type": "array",
+ "maxItems": 4,
+ "uniqueItems": true,
+ "items": { "enum": ["ctrl", "alt", "shift", "meta"] }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "button"],
+ "properties": {
+ "kind": { "const": "gamepad" },
+ "button": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]{0,39}$" },
+ "index": { "type": "integer", "minimum": 0, "maximum": 7, "description": "Which connected pad; omitted means any." }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "command"],
+ "properties": {
+ "kind": { "const": "chat_command" },
+ "command": { "type": "string", "pattern": "^![a-z0-9_-]{1,32}$", "description": "The command without arguments, lower case, e.g. !dance." }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "words"],
+ "properties": {
+ "kind": { "const": "chat_text" },
+ "words": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 16,
+ "uniqueItems": true,
+ "items": { "type": "string", "minLength": 1, "maxLength": 40 }
+ },
+ "mode": { "enum": ["any", "all"], "description": "any (default) matches one of the words; all matches every one." }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind"],
+ "properties": {
+ "kind": { "const": "follow" }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind"],
+ "properties": {
+ "kind": { "const": "tip" },
+ "min": { "type": "integer", "minimum": 0, "description": "The smallest tip, in Vibes, that fires the trigger; omitted means any tip." }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind"],
+ "properties": {
+ "kind": { "const": "ai_moment" }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind"],
+ "properties": {
+ "kind": { "const": "rundown_topic" },
+ "words": {
+ "type": "array",
+ "maxItems": 8,
+ "uniqueItems": true,
+ "items": { "type": "string", "minLength": 1, "maxLength": 40 },
+ "description": "Optional words the topic has to mention; omitted means any topic."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "every_seconds"],
+ "properties": {
+ "kind": { "const": "timer" },
+ "every_seconds": { "type": "integer", "minimum": 5, "maximum": 86400 },
+ "start": { "enum": ["live", "always"], "description": "live (default) counts only while the channel is live; always counts from the element being active." }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "cron", "timezone"],
+ "properties": {
+ "kind": { "const": "schedule" },
+ "cron": { "type": "string", "pattern": "^[0-9*/, -]{9,80}$", "description": "A five-field cron in the person's timezone." },
+ "timezone": { "type": "string", "minLength": 1, "maxLength": 64 }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "metric", "op", "value"],
+ "properties": {
+ "kind": { "const": "robot_telemetry" },
+ "metric": { "enum": ["battery_percent", "latency_ms", "stuck", "temperature_c"] },
+ "op": { "enum": ["lt", "gt"] },
+ "value": { "type": "number" },
+ "for_seconds": { "type": "integer", "minimum": 0, "maximum": 3600, "description": "How long the condition must hold; 0 (default) means at once." }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "event"],
+ "properties": {
+ "kind": { "const": "robot_event" },
+ "event": { "enum": ["estop", "stopped", "disconnected", "command_refused", "button"] }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind"],
+ "properties": {
+ "kind": { "const": "stream_start" }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind"],
+ "properties": {
+ "kind": { "const": "stream_end" }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "op", "count"],
+ "properties": {
+ "kind": { "const": "viewer_count" },
+ "op": { "enum": ["above", "below"] },
+ "count": { "type": "integer", "minimum": 0, "maximum": 100000000 }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "element_id"],
+ "properties": {
+ "kind": { "const": "element_event" },
+ "element_id": { "type": "string", "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "event": { "enum": ["pressed", "state", "shown"], "description": "pressed (default) fires after that element ran; state and shown fire when Live broadcasts them." }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
+{
+ "id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "schema": "ai.deep-dive@1",
+ "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "status": "finished",
+ "input": { "question": "a phone-first robot control panel", "depth": "quick", "max_sites": 6 },
+ "steps": [],
+ "sources": [],
+ "captures": [],
+ "analysis": [],
+ "synthesis": {
+ "patterns": [],
+ "comparison": { "columns": [], "rows": [] },
+ "best": { "source_id": "src_1", "what": "x", "evidence": [] },
+ "mistakes": [],
+ "gap": "",
+ "reference_tokens": { "palettes": [], "type_scales": [], "spacing": [] },
+ "do": [],
+ "dont": [],
+ "accessibility": [],
+ "speed": []
+ },
+ "recommendations": [],
+ "receipt": { "price_usd": 0.2, "held_usd": 0.2, "cost_usd": 0, "models": [], "captures_count": 0, "synthetic": false },
+ "gaps": [],
+ "created_at": "2026-11-11T10:00:00.000Z",
+ "updated_at": "2026-11-11T10:03:21.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
+{
+ "id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "schema": "ai.deep-dive@1",
+ "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "status": "succeeded",
+ "input": {
+ "question": "a phone-first robot control panel",
+ "depth": "quick",
+ "max_sites": 6,
+ "sites": ["https://example.com/robot"]
+ },
+ "steps": [
+ { "key": "plan", "status": "done", "detail": "wrote 9 expert questions", "started_at": "2026-11-11T10:00:00.000Z", "finished_at": "2026-11-11T10:00:02.000Z" },
+ { "key": "search", "status": "done", "detail": "4 engines, 12 sources", "started_at": "2026-11-11T10:00:02.000Z", "finished_at": "2026-11-11T10:00:40.000Z" },
+ { "key": "choose", "status": "done", "detail": "6 sites chosen", "started_at": "2026-11-11T10:00:40.000Z", "finished_at": "2026-11-11T10:00:42.000Z" },
+ { "key": "capture", "status": "done", "detail": "12 captures", "started_at": "2026-11-11T10:00:42.000Z", "finished_at": "2026-11-11T10:02:30.000Z" },
+ { "key": "analyse", "status": "done", "detail": "6 sites read", "started_at": "2026-11-11T10:02:30.000Z", "finished_at": "2026-11-11T10:03:05.000Z" },
+ { "key": "synthesise", "status": "done", "detail": "8 patterns", "started_at": "2026-11-11T10:03:05.000Z", "finished_at": "2026-11-11T10:03:20.000Z" },
+ { "key": "report", "status": "done", "detail": "report ready", "started_at": "2026-11-11T10:03:20.000Z", "finished_at": "2026-11-11T10:03:21.000Z" }
+ ],
+ "sources": [
+ {
+ "id": "src_1",
+ "kind": "web",
+ "title": "A phone-first robot control panel",
+ "url": "https://example.com/robot",
+ "publisher": "example.com",
+ "engine": "openvibe",
+ "snippet": "A control panel laid out for one thumb.",
+ "license": null,
+ "read_at": "2026-11-11T10:00:10.000Z"
+ }
+ ],
+ "captures": [
+ {
+ "id": "cap_1",
+ "source_id": "src_1",
+ "url": "https://example.com/robot",
+ "final_url": "https://example.com/robot",
+ "title": "A phone-first robot control panel",
+ "captured_at": "2026-11-11T10:00:45.000Z",
+ "viewport": 390,
+ "theme": "light",
+ "full_page": true,
+ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "width": 390,
+ "height": 2400,
+ "crops": [
+ { "id": "crop_1", "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPR", "y": 0, "height": 844 },
+ { "id": "crop_2", "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS", "y": 844, "height": 844 }
+ ],
+ "dom": { "fonts": { "families": ["Inter"], "sizes": ["14px", "16px", "28px"] }, "colours": [{ "value": "rgb(12, 14, 18)", "share": 0.71 }] },
+ "extras": null,
+ "skip": null,
+ "cached": false
+ }
+ ],
+ "analysis": [
+ {
+ "source_id": "src_1",
+ "url": "https://example.com/robot",
+ "summary": "One thumb reaches every primary control; the live camera fills the first screen.",
+ "first_impression": "The robot's camera frame is the page.",
+ "hierarchy": "One headline, one live frame, then the controls.",
+ "density": "Spacious; four controls per screen.",
+ "type": "One family, three sizes.",
+ "colour": "Near-black on white with a single amber action.",
+ "motion": "The frame pulses when connected.",
+ "words": "Verbs on buttons, state in the status line.",
+ "strengths": ["Every control sits in the bottom third", "State is written, not only coloured"],
+ "weaknesses": ["The emergency stop is the same size as the rest"],
+ "accessibility": "The stop button has no accessible name.",
+ "speed": "The first frame arrived once the socket opened; no skeleton.",
+ "evidence": [
+ { "claim": "Controls sit in the bottom third", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_2", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" },
+ { "claim": "The stop button has no accessible name", "source_id": "src_1", "capture_id": "cap_1", "crop_id": null, "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }
+ ]
+ }
+ ],
+ "synthesis": {
+ "patterns": [
+ {
+ "name": "Live view first",
+ "what": "The camera or state view is the whole first screen, controls below it.",
+ "sites": ["src_1"],
+ "usage_count": 1,
+ "evidence": [{ "claim": "The frame is the first screen", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_1", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }]
+ }
+ ],
+ "comparison": {
+ "columns": ["Site", "First screen", "Controls"],
+ "rows": [["example.com", "live frame", "bottom third"]]
+ },
+ "best": {
+ "source_id": "src_1",
+ "what": "The live frame doubles as the connection state.",
+ "evidence": [{ "claim": "The frame carries the status line", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_1", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }]
+ },
+ "mistakes": [
+ { "what": "The emergency stop is not separated from the other controls.", "evidence": [{ "claim": "All buttons share one row", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_2", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }] }
+ ],
+ "gap": "No control panel shows what the robot is about to do before the tap is sent.",
+ "reference_tokens": {
+ "palettes": [{ "source_id": "src_1", "colours": ["#0c0e12", "#ffffff", "#f5a623"] }],
+ "type_scales": [{ "source_id": "src_1", "families": ["Inter"], "sizes": ["14px", "16px", "28px"] }],
+ "spacing": [{ "source_id": "src_1", "steps": ["4px", "8px", "16px", "24px"] }]
+ },
+ "do": ["Keep every primary control in the thumb reach of one hand"],
+ "dont": ["Do not make the stop control share the row of ordinary actions"],
+ "accessibility": ["Name every icon-only control"],
+ "speed": ["Show the frame before the socket finishes connecting"]
+ },
+ "recommendations": [
+ {
+ "id": "rec_1",
+ "what": "Put the emergency stop alone, full width, at the bottom.",
+ "why": "The best panel suggests it by layout; the failure here is that it does not.",
+ "rule": { "id": "control.destructive-separation", "status": "follows", "text": "Destructive actions are separated from ordinary ones." },
+ "evidence": [{ "claim": "The stop shares the row", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_2", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }]
+ }
+ ],
+ "receipt": {
+ "price_usd": 0.2,
+ "held_usd": 0.2,
+ "cost_usd": 0.014,
+ "captures_count": 12,
+ "synthetic": false,
+ "models": [
+ { "step": "plan", "provider": "deepseek", "model": "deepseek-v4-flash", "tokens_in": 900, "tokens_out": 320, "cost_usd": 0.0004, "ms": 1800 },
+ { "step": "choose", "provider": "deepseek", "model": "deepseek-v4-flash", "tokens_in": 2400, "tokens_out": 500, "cost_usd": 0.0007, "ms": 2100 },
+ { "step": "analyse", "provider": "deepseek", "model": "deepseek-v4-pro", "tokens_in": 12000, "tokens_out": 4000, "cost_usd": 0.012, "ms": 26000 },
+ { "step": "synthesise", "provider": "deepseek", "model": "deepseek-v4-pro", "tokens_in": 6000, "tokens_out": 2500, "cost_usd": 0.007, "ms": 12000 }
+ ]
+ },
+ "gaps": ["No Expert extras were measured at Quick depth."],
+ "created_at": "2026-11-11T10:00:00.000Z",
+ "updated_at": "2026-11-11T10:03:21.000Z",
+ "finished_at": "2026-11-11T10:03:21.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
+{
+ "deep_dive_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "status": "running",
+ "step": "capture",
+ "progress": 4.5,
+ "steps": [],
+ "depth": "quick",
+ "requester": { "type": "service", "id": "exchange" },
+ "on_behalf_of": null,
+ "url": "https://openvibe.exchange/deep-dive/run_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "error": null,
+ "occurred_at": "2026-11-11T10:01:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "deep_dive_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "status": "running",
+ "step": "capture",
+ "progress": 0.5,
+ "steps": [
+ { "key": "plan", "status": "done" },
+ { "key": "search", "status": "done" },
+ { "key": "choose", "status": "done" },
+ { "key": "capture", "status": "running" },
+ { "key": "analyse", "status": "pending" },
+ { "key": "synthesise", "status": "pending" },
+ { "key": "report", "status": "pending" }
+ ],
+ "depth": "quick",
+ "requester": { "type": "service", "id": "exchange" },
+ "on_behalf_of": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" },
+ "url": "https://openvibe.exchange/deep-dive/run_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "error": null,
+ "occurred_at": "2026-11-11T10:01:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "channel": {
+ "username": "ada",
+ "display_name": "Ada"
+ },
+ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "element_name": "Make it dance",
+ "presser": {
+ "type": "user",
+ "id": "alice"
+ },
+ "presser_name": "Alice",
+ "trigger": {
+ "kind": "press"
+ },
+ "at": "2026-11-11T20:31:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+{
+ "channel": {
+ "username": "ada",
+ "display_name": "Ada"
+ },
+ "presser": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR"
+ },
+ "presser_name": "Maticus",
+ "trigger": {
+ "kind": "press"
+ },
+ "at": "2026-11-11T20:31:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+{
+ "channel": {
+ "username": "ada",
+ "display_name": "Ada"
+ },
+ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPS",
+ "element_name": "Headlight tally",
+ "presser": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR"
+ },
+ "presser_name": "Maticus",
+ "trigger": {
+ "kind": "chat_command",
+ "command": "!headlights"
+ },
+ "points_spent": 0,
+ "state_revision": 2,
+ "at": "2026-11-11T20:31:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "channel": {
+ "username": "ada",
+ "display_name": "Ada"
+ },
+ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "element_name": "Make it dance",
+ "presser": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR"
+ },
+ "presser_name": "Maticus",
+ "trigger": {
+ "kind": "press"
+ },
+ "points_spent": 50,
+ "state_revision": 4,
+ "at": "2026-11-11T20:31:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "channel": {
+ "username": "ada",
+ "display_name": "Ada"
+ },
+ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "revision": 1,
+ "values": {
+ "wheel": {
+ "entries": [
+ "a"
+ ]
+ }
+ },
+ "updated_at": "2026-11-11T20:31:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "channel": {
+ "username": "ada",
+ "display_name": "Ada"
+ },
+ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "revision": 4,
+ "values": {
+ "dances": 3,
+ "last_presser": "Maticus",
+ "wheel": [
+ "a",
+ "b",
+ 3
+ ],
+ "lit": true
+ },
+ "updated_at": "2026-11-11T20:31:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "surface": "bot.controls",
+ "device": "phone",
+ "layout_id": "lay_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "change": "applied",
+ "revision": 3
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "surface": "bot.controls",
+ "device": "phone",
+ "layout_id": "lay_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "change": "active",
+ "revision": 3,
+ "sharing": "private"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "surface": "bot.controls",
+ "device": "computer",
+ "layout_id": "lay_01JAB2C3D4E5F6G7H8J9K0MNPR",
+ "change": "published",
+ "revision": 7,
+ "sharing": "gallery"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "kind": "robot.say",
+ "product": "bot"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "kind": "robot.dance",
+ "product": "bot"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "kind": "robot.drive_pulse",
+ "product": "live",
+ "direction": "forward",
+ "ms": 600
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "kind": "chat.message",
+ "product": "chat",
+ "text": "Nice one!"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "kind": "robot.drive_pulse",
+ "product": "bot",
+ "direction": "forward",
+ "ms": 600
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "kind": "screen.alert",
+ "product": "live",
+ "title": "Dance!",
+ "text": "Ada made the robot dance."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+{
+ "channel": "ada",
+ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "revision": 1,
+ "values": {
+ "wheel": {
+ "entries": [
+ "a"
+ ]
+ }
+ },
+ "updated_at": "2026-11-11T20:31:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+{
+ "channel": "ada",
+ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "revision": 1,
+ "values": {
+ "Dances": 1
+ },
+ "updated_at": "2026-11-11T20:31:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "channel": "ada",
+ "element_id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "revision": 4,
+ "values": {
+ "dances": 3,
+ "last_presser": "Maticus",
+ "wheel": [
+ "a",
+ "b",
+ 3
+ ],
+ "lit": true
+ },
+ "updated_at": "2026-11-11T20:31:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "name": "Bad",
+ "author": {
+ "name": "Ada"
+ },
+ "sharing": "private",
+ "look": {
+ "component": "button",
+ "label": "Bad"
+ },
+ "recipe": {
+ "triggers": [
+ {
+ "kind": "press"
+ }
+ ]
+ },
+ "capabilities": [
+ "robot.say"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
+{
+ "name": "Bad",
+ "author": {
+ "name": "Ada"
+ },
+ "sharing": "private",
+ "look": {
+ "component": "button",
+ "label": "Bad"
+ },
+ "recipe": {
+ "triggers": [
+ {
+ "kind": "press"
+ }
+ ],
+ "actions": [
+ {
+ "kind": "robot.say",
+ "product": "bot",
+ "text": "hi"
+ }
+ ]
+ },
+ "capabilities": [
+ "robot.say"
+ ],
+ "initial_state": {
+ "wheel": {
+ "entries": [
+ "a"
+ ]
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
+{
+ "name": "Bad",
+ "author": {
+ "name": "Ada"
+ },
+ "sharing": "private",
+ "look": {
+ "component": "button",
+ "label": "Bad"
+ },
+ "recipe": {
+ "triggers": [
+ {
+ "kind": "press"
+ }
+ ],
+ "actions": [
+ {
+ "kind": "robot.dance",
+ "product": "bot"
+ }
+ ]
+ },
+ "capabilities": [
+ "robot.say"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
+{
+ "name": "Headlight tally",
+ "author": {
+ "name": "Ada"
+ },
+ "sharing": "private",
+ "look": {
+ "component": "counter",
+ "label": "Headlights",
+ "style": {
+ "tint": "surface",
+ "shape": "square",
+ "opacity": 0.8,
+ "show_label": true
+ }
+ },
+ "recipe": {
+ "triggers": [
+ {
+ "kind": "chat_command",
+ "command": "!headlights"
+ }
+ ],
+ "conditions": {
+ "role": "everyone",
+ "cooldown": {
+ "per_person_seconds": 30,
+ "overall_seconds": 5
+ }
+ },
+ "actions": [
+ {
+ "kind": "screen.counter",
+ "product": "live",
+ "key": "headlights",
+ "by": 1
+ },
+ {
+ "kind": "chat.message",
+ "product": "chat",
+ "text": "Headlights on!"
+ }
+ ]
+ },
+ "capabilities": [
+ "screen.counter",
+ "chat.message"
+ ],
+ "initial_state": {
+ "headlights": 0
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
+{
+ "id": "el_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "revision": 3,
+ "name": "Make it dance",
+ "description": "The robot spins for a moment and confetti follows.",
+ "author": {
+ "name": "Maticus",
+ "subject": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ }
+ },
+ "sharing": "gallery",
+ "look": {
+ "component": "button",
+ "label": "Make it dance",
+ "icon": "robot",
+ "props": {
+ "size": "large"
+ },
+ "style": {
+ "tint": "accent",
+ "shape": "pill",
+ "opacity": 1,
+ "show_label": true
+ }
+ },
+ "recipe": {
+ "triggers": [
+ {
+ "kind": "press"
+ }
+ ],
+ "conditions": {
+ "role": "follower",
+ "cost": {
+ "points": 50
+ },
+ "cooldown": {
+ "per_person_seconds": 60,
+ "overall_seconds": 10
+ },
+ "live_only": true,
+ "daily_cap": {
+ "per_person": 5
+ },
+ "chance": {
+ "one_in": 1
+ }
+ },
+ "actions": [
+ {
+ "kind": "robot.drive_pulse",
+ "product": "bot",
+ "direction": "spin_left",
+ "ms": 800,
+ "speed": 0.5
+ },
+ {
+ "kind": "screen.confetti",
+ "product": "live",
+ "intensity": "medium"
+ }
+ ]
+ },
+ "capabilities": [
+ "robot.drive_pulse",
+ "screen.confetti"
+ ],
+ "initial_state": {
+ "dances": 0
+ },
+ "created_at": "2026-11-11T20:00:00.000Z",
+ "updated_at": "2026-11-11T20:30:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
+{
+ "name": "Wheel of chatters",
+ "author": {
+ "name": "Ada"
+ },
+ "sharing": "link",
+ "look": {
+ "component": "wheel",
+ "label": "Spin the wheel"
+ },
+ "recipe": {
+ "triggers": [
+ {
+ "kind": "press"
+ },
+ {
+ "kind": "chat_command",
+ "command": "!wheel"
+ }
+ ],
+ "actions": [
+ {
+ "kind": "robot.say",
+ "product": "bot",
+ "text": "The wheel says hello."
+ }
+ ]
+ },
+ "capabilities": [
+ "robot.say"
+ ],
+ "mod": {
+ "id": "mod_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "version": "1.2.0",
+ "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "capabilities": [
+ "robot.say"
+ ]
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "catalog": {
+ "id": "bot.controls",
+ "version": 1,
+ "name": "Robot controls",
+ "devices": ["phone"],
+ "grid": { "columns": { "phone": 4, "tablet": 8, "computer": 12 }, "cell_px": { "phone": 44, "tablet": 44, "computer": 44 } },
+ "theme_tokens": ["danger"],
+ "parts": [
+ {
+ "id": "stop", "name": "Stop", "kind": "button",
+ "required": true, "pressed": true,
+ "min": { "w": 2, "h": 2 },
+ "styles": { "tints": ["danger"], "shapes": ["round"] },
+ "bindings": { "keys": ["Space"], "gamepad": ["south"] }
+ }
+ ]
+ },
+ "layout": null,
+ "words": "",
+ "device": "phone"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "catalog": {
+ "id": "bot.controls",
+ "version": 1,
+ "name": "Robot controls",
+ "devices": ["phone"],
+ "grid": { "columns": { "phone": 4, "tablet": 8, "computer": 12 }, "cell_px": { "phone": 44, "tablet": 44, "computer": 44 } },
+ "theme_tokens": ["danger"],
+ "parts": [
+ {
+ "id": "stop", "name": "Stop", "kind": "button",
+ "required": true, "pressed": true,
+ "min": { "w": 2, "h": 2 },
+ "styles": { "tints": ["danger"], "shapes": ["round"] },
+ "bindings": { "keys": ["Space"], "gamepad": ["south"] }
+ }
+ ]
+ },
+ "layout": null,
+ "words": "make it work with one thumb",
+ "device": "phone"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
+{
+ "catalog": {
+ "id": "bot.controls",
+ "version": 1,
+ "name": "Robot controls",
+ "devices": ["phone"],
+ "grid": { "columns": { "phone": 4, "tablet": 8, "computer": 12 }, "cell_px": { "phone": 44, "tablet": 44, "computer": 44 } },
+ "theme_tokens": ["danger", "surface"],
+ "parts": [
+ {
+ "id": "stop", "name": "Stop", "kind": "button",
+ "required": true, "pressed": true,
+ "min": { "w": 2, "h": 2, "px": 64 },
+ "styles": { "tints": ["danger"], "shapes": ["round"] },
+ "bindings": { "keys": ["Space"], "gamepad": ["south"] }
+ },
+ {
+ "id": "stick", "name": "Drive stick", "kind": "stick",
+ "required": false, "pressed": true,
+ "min": { "w": 3, "h": 3 },
+ "styles": { "tints": ["surface"], "shapes": ["round"] },
+ "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] }
+ }
+ ]
+ },
+ "layout": {
+ "surface": "bot.controls",
+ "surface_version": 1,
+ "device": "phone",
+ "name": "Default",
+ "author": { "name": "OpenVibe" },
+ "sharing": "private",
+ "parts": [
+ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": ["Space"], "gamepad": ["south"] } },
+ { "id": "stick", "visible": true, "place": { "x": 0, "y": 2, "w": 4, "h": 3 }, "style": { "tint": "surface", "shape": "round", "opacity": 1, "label": false }, "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] } }
+ ]
+ },
+ "words": "put Stop on the left and make it huge, and give me a vertical head slider on the right edge",
+ "device": "phone"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "ok": false,
+ "layout": {
+ "surface": "bot.controls",
+ "surface_version": 1,
+ "device": "phone",
+ "name": "Still here",
+ "author": { "name": "Ana" },
+ "sharing": "private",
+ "parts": [
+ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": [], "gamepad": [] } }
+ ]
+ },
+ "summary": "Couldn't do that.",
+ "reasons": ["Stop has to stay visible."]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "ok": true,
+ "layout": null,
+ "summary": "Put Stop on the left.",
+ "reasons": []
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "ok": false,
+ "layout": null,
+ "summary": "Couldn't do that: Stop has to stay visible.",
+ "reasons": ["Stop has to stay visible.", "Every pressed part has to be at least 44 px."],
+ "receipt": { "model": "deepseek-flash", "provider": "deepseek", "route": "ui.layout.suggest", "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPR" }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "ok": true,
+ "layout": {
+ "surface": "bot.controls",
+ "surface_version": 1,
+ "device": "phone",
+ "name": "One-thumb controls",
+ "author": { "name": "Ana" },
+ "sharing": "private",
+ "parts": [
+ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 4, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": ["Space"], "gamepad": ["south"] } },
+ { "id": "stick", "visible": true, "place": { "x": 0, "y": 2, "w": 4, "h": 3 }, "style": { "tint": "surface", "shape": "round", "opacity": 1, "label": false }, "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] } }
+ ]
+ },
+ "summary": "Put Stop across the top and the drive stick below it where a thumb reaches.",
+ "reasons": ["Stop had to stay visible, so it became the top row.", "The stick moved under the thumb."],
+ "receipt": { "model": "deepseek-flash", "provider": "deepseek", "route": "ui.layout.suggest", "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPQ", "cost_usd": 0.0004 }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "surface": "bot.controls",
+ "surface_version": 1,
+ "device": "phone",
+ "name": "Bad tint",
+ "author": { "name": "Ana" },
+ "sharing": "private",
+ "parts": [
+ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "#ff0000", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": [], "gamepad": [] } }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "surface": "bot.controls",
+ "surface_version": 1,
+ "device": "phone",
+ "name": "Too faint",
+ "author": { "name": "Ana" },
+ "sharing": "private",
+ "parts": [
+ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 0.2, "label": true }, "bindings": { "keys": [], "gamepad": [] } }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
+{
+ "surface": "bot.overlay",
+ "surface_version": 1,
+ "device": "computer",
+ "name": "One-thumb overlay",
+ "author": {
+ "name": "Ana"
+ },
+ "sharing": "link",
+ "parts": [
+ {
+ "id": "stop",
+ "visible": true,
+ "place": {
+ "dock": "right",
+ "order": 0,
+ "w": 2,
+ "h": 1
+ },
+ "style": {
+ "tint": "danger",
+ "shape": "round",
+ "opacity": 1,
+ "label": true
+ },
+ "bindings": {
+ "keys": [
+ "Space"
+ ],
+ "gamepad": [
+ "south"
+ ]
+ }
+ },
+ {
+ "id": "battery",
+ "visible": true,
+ "place": {
+ "dock": "right",
+ "order": 1,
+ "w": 2,
+ "h": 1
+ },
+ "style": {
+ "tint": "surface",
+ "shape": "pill",
+ "opacity": 0.8,
+ "label": false
+ },
+ "bindings": {
+ "keys": [],
+ "gamepad": []
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "surface": "bot.controls",
+ "surface_version": 1,
+ "device": "phone",
+ "name": "My phone controls",
+ "author": { "name": "Ana", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" } },
+ "sharing": "private",
+ "parts": [
+ { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": ["Space"], "gamepad": ["south"] } },
+ { "id": "estop", "visible": true, "place": { "x": 2, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "square", "opacity": 1, "label": true }, "bindings": { "keys": ["Escape"], "gamepad": ["start"] } },
+ { "id": "stick", "visible": true, "place": { "x": 0, "y": 2, "w": 4, "h": 3 }, "style": { "tint": "surface", "shape": "round", "opacity": 1, "label": false }, "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] } },
+ { "id": "battery", "visible": true, "place": { "x": 0, "y": 5, "w": 2, "h": 1 }, "style": { "tint": "success", "shape": "pill", "opacity": 1, "label": true }, "bindings": { "keys": [], "gamepad": [] } },
+ { "id": "latency", "visible": false, "place": { "x": 2, "y": 5, "w": 2, "h": 1 }, "style": { "tint": "muted", "shape": "pill", "opacity": 1, "label": true }, "bindings": { "keys": [], "gamepad": [] } }
+ ],
+ "updated_at": "2026-11-11T12:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "id": "bot.controls",
+ "version": 1,
+ "name": "Robot controls",
+ "devices": ["phone"],
+ "grid": { "columns": { "phone": 4, "tablet": 8, "computer": 12 }, "cell_px": { "phone": 44, "tablet": 44, "computer": 44 } },
+ "theme_tokens": ["danger"],
+ "parts": [
+ {
+ "id": "stop", "name": "Stop", "kind": "button",
+ "required": true, "pressed": true,
+ "min": { "w": 2, "h": 2 },
+ "styles": { "tints": ["#ff0000"], "shapes": ["round"] },
+ "bindings": { "keys": ["Space"], "gamepad": ["south"] }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "id": "bot.controls",
+ "version": 1,
+ "name": "Robot controls",
+ "devices": ["phone"],
+ "grid": { "columns": { "phone": 4, "tablet": 8, "computer": 12 }, "cell_px": { "phone": 44, "tablet": 44, "computer": 44 } },
+ "theme_tokens": ["accent"],
+ "parts": [
+ {
+ "id": "stop", "name": "Stop", "kind": "power",
+ "required": true, "pressed": true,
+ "min": { "w": 2, "h": 2 },
+ "styles": { "tints": ["danger"], "shapes": ["round"] },
+ "bindings": { "keys": ["Space"], "gamepad": ["south"] }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
+{
+ "id": "bot.controls",
+ "version": 1,
+ "name": "Robot controls",
+ "description": "The robot's own panel: Stop and the e-stop always visible, then the stick, the head slider and the readouts.",
+ "devices": ["phone", "tablet", "computer"],
+ "grid": {
+ "columns": { "phone": 4, "tablet": 8, "computer": 12 },
+ "cell_px": { "phone": 44, "tablet": 44, "computer": 44 }
+ },
+ "theme_tokens": ["accent", "surface", "surface-raised", "text", "muted", "danger", "warning", "success"],
+ "parts": [
+ {
+ "id": "stop", "name": "Stop", "kind": "button",
+ "required": true, "pressed": true,
+ "min": { "w": 2, "h": 2, "px": 64 },
+ "sizes": [{ "w": 2, "h": 2 }, { "w": 3, "h": 2 }],
+ "styles": { "tints": ["danger", "warning"], "shapes": ["round", "pill", "square"], "opacity": { "min": 0.7, "max": 1 }, "label": true },
+ "bindings": { "keys": ["Space", "Escape"], "gamepad": ["south", "start"] },
+ "never_covers": []
+ },
+ {
+ "id": "estop", "name": "E-stop", "kind": "button",
+ "required": true, "pressed": true,
+ "min": { "w": 2, "h": 2, "px": 64 },
+ "styles": { "tints": ["danger"], "shapes": ["round", "square"], "opacity": { "min": 0.8, "max": 1 }, "label": true },
+ "bindings": { "keys": ["Escape"], "gamepad": ["start"] },
+ "never_covers": []
+ },
+ {
+ "id": "stick", "name": "Drive stick", "kind": "stick",
+ "required": false, "pressed": true,
+ "min": { "w": 3, "h": 3 },
+ "sizes": [{ "w": 3, "h": 3 }, { "w": 4, "h": 4 }],
+ "styles": { "tints": ["surface", "surface-raised"], "shapes": ["round"], "opacity": { "min": 0.6, "max": 1 }, "label": false },
+ "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] },
+ "never_covers": ["stop", "estop"]
+ },
+ {
+ "id": "head", "name": "Head", "kind": "slider",
+ "required": false, "pressed": true,
+ "min": { "w": 2, "h": 2 },
+ "styles": { "tints": ["accent", "surface-raised"], "shapes": ["pill", "square"], "opacity": { "min": 0.4, "max": 1 }, "label": true },
+ "bindings": { "keys": ["ArrowUp", "ArrowDown"], "gamepad": ["right-stick-y"] },
+ "never_covers": ["stop", "estop"]
+ },
+ {
+ "id": "battery", "name": "Battery", "kind": "readout",
+ "required": false, "pressed": false,
+ "min": { "w": 1, "h": 1 },
+ "styles": { "tints": ["text", "muted", "success"], "shapes": ["pill", "square"], "opacity": { "min": 0.4, "max": 1 }, "label": true },
+ "bindings": { "keys": [], "gamepad": [] },
+ "never_covers": ["stop", "estop"]
+ },
+ {
+ "id": "latency", "name": "Latency", "kind": "readout",
+ "required": false, "pressed": false,
+ "min": { "w": 1, "h": 1 },
+ "styles": { "tints": ["text", "muted"], "shapes": ["pill", "square"], "opacity": { "min": 0.4, "max": 1 }, "label": true },
+ "bindings": { "keys": [], "gamepad": [] },
+ "never_covers": ["stop", "estop"]
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "kind": "chat_command",
+ "command": "dance"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "kind": "hold",
+ "ms": 10
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "kind": "cheer"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "kind": "chat_command",
+ "command": "!dance"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "kind": "press"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "kind": "robot_telemetry",
+ "metric": "battery_percent",
+ "op": "lt",
+ "value": 20,
+ "for_seconds": 30
+}
@@ -130805,6 +130805,3579 @@
@@ -130805,6 +130805,3579 @@
}
}
}
}
}
}
+ },
+ "ui.surface": {
+ "version": "1.0.0",
+ "owner": "contracts",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/surface.v1.json",
+ "title": "UiSurface",
+ "description": "ui.surface@1: the catalog a product publishes for one surface people may arrange (bot.controls, bot.overlay, live.channel, dock, chat.composer, navbar.shortcuts). It names the surface, its device classes and grid, the theme tokens its styles draw from, and every part a layout may place. A part says what it is (stick, button, slider, readout, video, list), whether it is required or pressed, its minimum cells and the minimum pixels the product guarantees (44 px for anything pressed, larger for Stop), the sizes and styles it allows, the bindings it accepts, and the parts it must never cover. A layout is validated against this catalog (openvibe-sdk/ui-layout); a layout can arrange only what the catalog has and can never add a power. The catalog is data: a layout or an AI answer never carries code or a URL.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "version",
+ "name",
+ "devices",
+ "grid",
+ "theme_tokens",
+ "parts"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",
+ "maxLength": 80,
+ "description": "The surface id: <product>.<surface>, e.g. bot.controls."
+ },
+ "version": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The catalog version. A layout names the version it was made against."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "What the surface is called where a person sees it."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 500
+ },
+ "devices": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 3,
+ "uniqueItems": true,
+ "items": {
+ "enum": [
+ "phone",
+ "tablet",
+ "computer"
+ ]
+ },
+ "description": "The device classes this surface offers."
+ },
+ "grid": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "columns",
+ "cell_px"
+ ],
+ "properties": {
+ "columns": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "phone",
+ "tablet",
+ "computer"
+ ],
+ "description": "The grid's columns per device class: 4 on a phone and 12 on a computer in the design; a product may choose otherwise within these bounds.",
+ "properties": {
+ "phone": {
+ "type": "integer",
+ "minimum": 2,
+ "maximum": 8
+ },
+ "tablet": {
+ "type": "integer",
+ "minimum": 2,
+ "maximum": 16
+ },
+ "computer": {
+ "type": "integer",
+ "minimum": 2,
+ "maximum": 24
+ }
+ }
+ },
+ "cell_px": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "phone",
+ "tablet",
+ "computer"
+ ],
+ "description": "The smallest a cell is rendered at that device class, in CSS pixels, at the narrowest supported width. The validator multiplies a part's cells by this to check the 44 px rule.",
+ "properties": {
+ "phone": {
+ "type": "integer",
+ "minimum": 24,
+ "maximum": 200
+ },
+ "tablet": {
+ "type": "integer",
+ "minimum": 24,
+ "maximum": 200
+ },
+ "computer": {
+ "type": "integer",
+ "minimum": 24,
+ "maximum": 200
+ }
+ }
+ }
+ }
+ },
+ "theme_tokens": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,39}$"
+ },
+ "description": "The theme tokens a tint may name (accent, surface, danger, ...). Never a colour value: the theme decides what the token is."
+ },
+ "parts": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 64,
+ "items": {
+ "$ref": "#/$defs/part"
+ }
+ }
+ },
+ "$defs": {
+ "part": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "name",
+ "kind",
+ "required",
+ "pressed",
+ "min",
+ "styles",
+ "bindings"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$",
+ "description": "Stable part id inside this catalog, e.g. stop, stick, battery."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "kind": {
+ "enum": [
+ "stick",
+ "button",
+ "slider",
+ "readout",
+ "video",
+ "list"
+ ]
+ },
+ "required": {
+ "type": "boolean",
+ "description": "A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused."
+ },
+ "pressed": {
+ "type": "boolean",
+ "description": "The part sends a command when pressed or moved; it must render at least 44 px on every side."
+ },
+ "min": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "w",
+ "h"
+ ],
+ "description": "The smallest size a layout may give the part: w and h in grid cells, and px when the product guarantees more than 44 px (Stop).",
+ "properties": {
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "px": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 400
+ }
+ }
+ },
+ "sizes": {
+ "type": "array",
+ "maxItems": 12,
+ "uniqueItems": true,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "w",
+ "h"
+ ],
+ "properties": {
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ }
+ }
+ },
+ "description": "Sizes the editor may offer as presets. A layout is not limited to these: it is limited to min and to the grid."
+ },
+ "styles": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "tints",
+ "shapes"
+ ],
+ "properties": {
+ "tints": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 16,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,39}$"
+ },
+ "description": "The theme tokens this part may be tinted with; every one is also in the surface's theme_tokens."
+ },
+ "shapes": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 3,
+ "uniqueItems": true,
+ "items": {
+ "enum": [
+ "round",
+ "pill",
+ "square"
+ ]
+ }
+ },
+ "opacity": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "min",
+ "max"
+ ],
+ "properties": {
+ "min": {
+ "type": "number",
+ "minimum": 0.4,
+ "maximum": 1
+ },
+ "max": {
+ "type": "number",
+ "minimum": 0.4,
+ "maximum": 1
+ }
+ },
+ "description": "The opacity range this part allows; the design's range is 0.4 to 1."
+ },
+ "label": {
+ "type": "boolean",
+ "description": "Whether the editor may show the part's label on it."
+ }
+ }
+ },
+ "bindings": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "keys",
+ "gamepad"
+ ],
+ "properties": {
+ "keys": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"
+ }
+ },
+ "gamepad": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ }
+ }
+ },
+ "description": "The bindings the product accepts for this part. A layout may pick a subset, never add one."
+ },
+ "never_covers": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ },
+ "description": "The parts this part must never cover, in addition to the surface-wide rule that nothing covers a required part."
+ }
+ }
+ }
+ }
+ }
+ },
+ "ui.layout": {
+ "version": "1.0.0",
+ "owner": "contracts",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/layout.v1.json",
+ "title": "UiLayout",
+ "description": "ui.layout@1: one person's arrangement of one surface on one device class. A layout names the surface and its catalog version, the device, its own name, its author and its sharing (private, link or gallery), and one entry per part it places. A part is visible or not, sits on the surface's grid (x, y, w, h in cells) or docks to an edge for an overlay, and carries a style (a theme token tint, a shape, opacity 0.4-1 and the label) and bindings. It is data only: no URL, no markup and no code may appear anywhere in it, and every part id, tint, shape, opacity, size and binding must be one the catalog allows. openvibe-sdk/ui-layout validates it before any server, editor or AI answer stores or shows it.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "surface",
+ "surface_version",
+ "device",
+ "name",
+ "author",
+ "sharing",
+ "parts"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Assigned by OpenVibe.Network when the layout is stored; absent in a draft, a share answer or an AI suggestion."
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Assigned by the store; every change to a stored layout moves it by one."
+ },
+ "active": {
+ "type": "boolean",
+ "description": "Set by OpenVibe.Network: the one layout a product draws for this surface on this device class. A draft, a share answer and an AI suggestion carry no active field."
+ },
+ "surface": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",
+ "maxLength": 80
+ },
+ "surface_version": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "device": {
+ "enum": [
+ "phone",
+ "tablet",
+ "computer"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "The person's name for this layout; \"My phone controls\" is fine."
+ },
+ "author": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "subject": {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ },
+ "description": "The author's display name and, for a private or link layout, the subject. A share or gallery answer carries the display name only, never the account."
+ },
+ "sharing": {
+ "enum": [
+ "private",
+ "link",
+ "gallery"
+ ]
+ },
+ "parts": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 64,
+ "items": {
+ "$ref": "#/$defs/part"
+ }
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "$defs": {
+ "place": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "x",
+ "y",
+ "w",
+ "h"
+ ],
+ "properties": {
+ "x": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 23
+ },
+ "y": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100
+ },
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ }
+ },
+ "description": "The part's cell on the surface grid."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "dock",
+ "order",
+ "w",
+ "h"
+ ],
+ "properties": {
+ "dock": {
+ "enum": [
+ "left",
+ "right",
+ "top",
+ "bottom"
+ ]
+ },
+ "order": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 63
+ },
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ }
+ },
+ "description": "For an overlay: the edge the part docks to, its order along that edge, and its size in cells."
+ }
+ ]
+ },
+ "style": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "tint",
+ "shape",
+ "opacity",
+ "label"
+ ],
+ "properties": {
+ "tint": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,39}$",
+ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."
+ },
+ "shape": {
+ "enum": [
+ "round",
+ "pill",
+ "square"
+ ]
+ },
+ "opacity": {
+ "type": "number",
+ "minimum": 0.4,
+ "maximum": 1
+ },
+ "label": {
+ "type": "boolean"
+ }
+ }
+ },
+ "bindings": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "keys",
+ "gamepad"
+ ],
+ "properties": {
+ "keys": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"
+ }
+ },
+ "gamepad": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ }
+ }
+ }
+ },
+ "part": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "visible",
+ "place",
+ "style",
+ "bindings"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ },
+ "visible": {
+ "type": "boolean"
+ },
+ "place": {
+ "$ref": "#/$defs/place"
+ },
+ "style": {
+ "$ref": "#/$defs/style"
+ },
+ "bindings": {
+ "$ref": "#/$defs/bindings"
+ }
+ }
+ }
+ }
+ }
+ },
+ "ui.layout-request": {
+ "version": "1.0.0",
+ "owner": "contracts",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/layout-request.v1.json",
+ "title": "UiLayoutRequest",
+ "description": "ui.layout-request@1: what an editor sends OpenVibe.AI's ui.layout.suggest task, and what Network forwards for a signed-in person. It is the surface's catalog, the layout shown now (or null for the default one) and the words the person typed (max 500 characters), for one device class. The catalog is the only power the answer can use; the current layout is the starting arrangement; the words never contain markup that becomes part of it.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "catalog",
+ "layout",
+ "words",
+ "device"
+ ],
+ "properties": {
+ "catalog": {
+ "$ref": "surface.v1.json"
+ },
+ "layout": {
+ "oneOf": [
+ {
+ "$ref": "layout.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The arrangement shown now, or null when the person is looking at the default."
+ },
+ "words": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500,
+ "description": "The person's own description, e.g. \"put Stop on the left and make it huge\"."
+ },
+ "device": {
+ "enum": [
+ "phone",
+ "tablet",
+ "computer"
+ ]
+ }
+ }
+ }
+ },
+ "ui.layout-suggestion": {
+ "version": "1.0.0",
+ "owner": "contracts",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/layout-suggestion.v1.json",
+ "title": "UiLayoutSuggestion",
+ "description": "ui.layout-suggestion@1: OpenVibe.AI's answer to ui.layout.suggest. ok true carries the layout, already checked by openvibe-sdk/ui-layout against the catalog sent with the request, and a one-sentence summary of what changed plus the model's reasons. ok false is an honest refusal: no layout, one sentence saying what it could not do, and the validator's reasons in plain words (for example \"Stop has to stay visible.\"). A receipt names the model that answered, with the run and the route. The suggestion is previewed, never applied silently; Network or the product stores it only after the person accepts it.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "ok",
+ "layout",
+ "summary",
+ "reasons"
+ ],
+ "properties": {
+ "ok": {
+ "type": "boolean"
+ },
+ "layout": {
+ "oneOf": [
+ {
+ "$ref": "layout.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The validated layout when ok is true; null when it is false."
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300,
+ "description": "One sentence saying what changed, or what could not be done."
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ },
+ "description": "Why this arrangement, or the validator's reasons for refusing one."
+ },
+ "receipt": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "model"
+ ],
+ "properties": {
+ "model": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "provider": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "route": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "run_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "cost_usd": {
+ "type": "number",
+ "minimum": 0
+ }
+ },
+ "description": "Which model answered: the same receipt every AI run carries, at the task's own shape."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "ok": {
+ "const": true
+ }
+ },
+ "required": [
+ "ok"
+ ]
+ },
+ "then": {
+ "properties": {
+ "layout": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "ok": {
+ "const": false
+ }
+ },
+ "required": [
+ "ok"
+ ]
+ },
+ "then": {
+ "properties": {
+ "layout": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "network.layout.changed": {
+ "version": "1.0.0",
+ "owner": "network",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/network.layout.changed.v1.json",
+ "title": "NetworkLayoutChangedPayload",
+ "description": "network.layout.changed v1 (OpenVibe.Network server/layouts, written into network_event_outbox in the transaction that changes a stored layout). One event per change to one person's ui.layout@1: set active, saved, renamed, deleted, shared, unshared, published to the gallery, unpublished, copied from the gallery, or reset to the default. Envelope: subject { type: user, id } and the layout's revision, actor the person, visibility internal. The payload carries no account data and no layout contents, so an open page can announce a change and refetch with the person's own token.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "surface",
+ "device",
+ "layout_id",
+ "change",
+ "revision"
+ ],
+ "properties": {
+ "surface": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",
+ "maxLength": 80
+ },
+ "device": {
+ "enum": [
+ "phone",
+ "tablet",
+ "computer"
+ ]
+ },
+ "layout_id": {
+ "type": "string",
+ "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "change": {
+ "enum": [
+ "active",
+ "saved",
+ "renamed",
+ "deleted",
+ "shared",
+ "unshared",
+ "published",
+ "unpublished",
+ "copied",
+ "reset"
+ ]
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "sharing": {
+ "enum": [
+ "private",
+ "link",
+ "gallery"
+ ]
+ }
+ }
+ }
+ },
+ "ui.action": {
+ "version": "1.0.0",
+ "owner": "contracts",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/action.v1.json",
+ "title": "UiAction",
+ "description": "ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop, the e-stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.",
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "direction",
+ "ms"
+ ],
+ "description": "Drive the robot for a moment, then stop. The duration is bounded by Bot's own max_command_ms and the owner's limits for the presser's role.",
+ "properties": {
+ "kind": {
+ "const": "robot.drive_pulse"
+ },
+ "product": {
+ "const": "bot"
+ },
+ "direction": {
+ "enum": [
+ "forward",
+ "back",
+ "left",
+ "right",
+ "spin_left",
+ "spin_right"
+ ]
+ },
+ "ms": {
+ "type": "integer",
+ "minimum": 50,
+ "maximum": 3000
+ },
+ "speed": {
+ "type": "number",
+ "minimum": 0.1,
+ "maximum": 1,
+ "description": "Fraction of the robot's allowed speed; omitted means the owner's default for the role."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "preset"
+ ],
+ "description": "Take the robot's head to a preset. Bot maps up, center and down to the head actuator its robot profile declares; a robot without one refuses it.",
+ "properties": {
+ "kind": {
+ "const": "robot.head_preset"
+ },
+ "product": {
+ "const": "bot"
+ },
+ "preset": {
+ "enum": [
+ "up",
+ "center",
+ "down"
+ ],
+ "description": "A head tilt preset. Bot still checks the robot profile and the owner's limits."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "on"
+ ],
+ "description": "Turn the robot's headlight on or off. Bot's own headlight kind is the authority: only the owner and People Operators may send it, and a robot whose profile has no headlight refuses it.",
+ "properties": {
+ "kind": {
+ "const": "robot.headlight"
+ },
+ "product": {
+ "const": "bot"
+ },
+ "on": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "text"
+ ],
+ "description": "Speak a line through the robot. Text goes through the existing content checks and Bot's own speak limits.",
+ "properties": {
+ "kind": {
+ "const": "robot.say"
+ },
+ "product": {
+ "const": "bot"
+ },
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "macro"
+ ],
+ "description": "Replay a recorded robot macro through the safe command path, with its own deadline. Macros are stage 4 of Elements; a stage-1 product refuses this kind as not yet supported.",
+ "properties": {
+ "kind": {
+ "const": "robot.macro"
+ },
+ "product": {
+ "const": "bot"
+ },
+ "macro": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "text"
+ ],
+ "description": "Send a chat message as the channel's bot. Text goes through Chat's content checks and rate limits.",
+ "properties": {
+ "kind": {
+ "const": "chat.message"
+ },
+ "product": {
+ "const": "chat"
+ },
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 400
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "source"
+ ],
+ "properties": {
+ "kind": {
+ "const": "chat.pin"
+ },
+ "product": {
+ "const": "chat"
+ },
+ "source": {
+ "enum": [
+ "trigger",
+ "latest"
+ ],
+ "description": "trigger pins the message that caused this action; latest pins the newest chat message."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "question",
+ "options"
+ ],
+ "properties": {
+ "kind": {
+ "const": "chat.poll"
+ },
+ "product": {
+ "const": "chat"
+ },
+ "question": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "options": {
+ "type": "array",
+ "minItems": 2,
+ "maxItems": 10,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "title"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.alert"
+ },
+ "product": {
+ "const": "live"
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "name"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.animation"
+ },
+ "product": {
+ "const": "live"
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "An animation the page or overlay has; an unknown name is refused, never fetched."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.confetti"
+ },
+ "product": {
+ "const": "live"
+ },
+ "intensity": {
+ "enum": [
+ "low",
+ "medium",
+ "high"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "sound"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.sound"
+ },
+ "product": {
+ "const": "live"
+ },
+ "sound": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "A sound from the streamer's own sound list; Live never fetches a URL for it."
+ },
+ "volume": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "text",
+ "voice"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.tts"
+ },
+ "product": {
+ "const": "live"
+ },
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ },
+ "voice": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "A voice from the streamer's own voice list."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "key",
+ "by"
+ ],
+ "description": "Move one of the element's counters. The host applies it to the element's own state and Live broadcasts the new state.",
+ "properties": {
+ "kind": {
+ "const": "screen.counter"
+ },
+ "product": {
+ "const": "live"
+ },
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{0,31}$"
+ },
+ "by": {
+ "type": "integer",
+ "minimum": -1000,
+ "maximum": 1000
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "key",
+ "value"
+ ],
+ "description": "Set one of the element's state values: a boolean, a number, a short string or a list of those (a wheel's entries, a scoreboard). Objects are not state values.",
+ "properties": {
+ "kind": {
+ "const": "screen.state"
+ },
+ "product": {
+ "const": "live"
+ },
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{0,31}$"
+ },
+ "value": {
+ "oneOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "maxLength": 200
+ },
+ {
+ "type": "array",
+ "maxItems": 64,
+ "items": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "maxLength": 200
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "element",
+ "visible"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.visibility"
+ },
+ "product": {
+ "const": "live"
+ },
+ "element": {
+ "type": "string",
+ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "scene"
+ ],
+ "properties": {
+ "kind": {
+ "const": "screen.scene"
+ },
+ "product": {
+ "const": "live"
+ },
+ "scene": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "A scene the layout has; an unknown scene is refused."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product"
+ ],
+ "properties": {
+ "kind": {
+ "const": "stream.clip"
+ },
+ "product": {
+ "const": "live"
+ },
+ "seconds": {
+ "type": "integer",
+ "minimum": 5,
+ "maximum": 30,
+ "description": "How far back the clip starts, at most 30 seconds."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product"
+ ],
+ "properties": {
+ "kind": {
+ "const": "stream.moment"
+ },
+ "product": {
+ "const": "live"
+ },
+ "label": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "title"
+ ],
+ "properties": {
+ "kind": {
+ "const": "stream.title"
+ },
+ "product": {
+ "const": "live"
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 140
+ }
+ },
+ "description": "Change the stream title. Live allows it only for the channel's owner, whatever the element's trigger says."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "category"
+ ],
+ "properties": {
+ "kind": {
+ "const": "stream.category"
+ },
+ "product": {
+ "const": "live"
+ },
+ "category": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ }
+ },
+ "description": "Change the stream category. Live allows it only for the channel's owner."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "prompt"
+ ],
+ "description": "Ask an AI model through Exchange's catalogue, with the element's budget and a receipt. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",
+ "properties": {
+ "kind": {
+ "const": "ai.ask"
+ },
+ "product": {
+ "const": "ai"
+ },
+ "prompt": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "model": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "max_cost_usd": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "task"
+ ],
+ "description": "Hand a task to an Actor agent. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",
+ "properties": {
+ "kind": {
+ "const": "actor.task"
+ },
+ "product": {
+ "const": "actor"
+ },
+ "task": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 8000
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "endpoint"
+ ],
+ "description": "Call one of the streamer's own webhook endpoints. The element names the endpoint the streamer listed; it never carries a URL, and the payload never carries a viewer's data. Stage 2 of Elements; a stage-1 product refuses it as not yet supported.",
+ "properties": {
+ "kind": {
+ "const": "webhook.call"
+ },
+ "product": {
+ "const": "live"
+ },
+ "endpoint": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_-]{0,31}$"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "product",
+ "goal"
+ ],
+ "description": "Update a tip goal's display. Money never moves here; Billing's own sheet confirms any charge. Stage 4 of Elements.",
+ "properties": {
+ "kind": {
+ "const": "money.tip_goal"
+ },
+ "product": {
+ "const": "billing"
+ },
+ "goal": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40
+ }
+ }
+ }
+ ],
+ "$defs": {
+ "capability": {
+ "description": "The action kinds an Element may declare in ui.element@1 capabilities. One id, one product: the host routes an action only to the product that owns it.",
+ "enum": [
+ "robot.drive_pulse",
+ "robot.head_preset",
+ "robot.headlight",
+ "robot.say",
+ "robot.macro",
+ "chat.message",
+ "chat.pin",
+ "chat.poll",
+ "screen.alert",
+ "screen.animation",
+ "screen.confetti",
+ "screen.sound",
+ "screen.tts",
+ "screen.counter",
+ "screen.state",
+ "screen.visibility",
+ "screen.scene",
+ "stream.clip",
+ "stream.moment",
+ "stream.title",
+ "stream.category",
+ "ai.ask",
+ "actor.task",
+ "webhook.call",
+ "money.tip_goal"
+ ]
+ }
+ }
+ }
+ },
+ "ui.element": {
+ "version": "1.0.0",
+ "owner": "contracts",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/element.v1.json",
+ "title": "UiElement",
+ "description": "ui.element@1: an Element people make and place on a surface: a look, a behaviour and, only when a recipe is not enough, a mod. LOOK draws from OpenVibe.Design's components and the theme's tokens, so it follows the person's theme, the seasons and dark/light; it carries no markup and no URL. RECIPE is data only: one or more triggers from ui.trigger@1, the conditions that decide who may fire it and what it costs, and one or more actions from ui.action@1, checked against CAPABILITIES. A capability is an action kind the element declares; the host refuses an action the element did not declare, and the product named by the action still checks the presser's role, the cooldown, the cost and its own limits. MOD is an optional reference to a code module (id, version and digest) for the stage where a recipe is not enough; the module never adds a power. Conditions use channel points only in stage 1: no Vibes, no money, and Billing still confirms any charge itself. The id and revision are assigned by the store; a draft has neither.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "author",
+ "sharing",
+ "look",
+ "recipe",
+ "capabilities"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Assigned by the store when the element is saved; absent in a draft."
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Assigned by the store; every saved change moves it by one."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 500
+ },
+ "author": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "subject": {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ },
+ "description": "The author's display name and, for a private or link element, the subject. A gallery or copy answer carries the display name only, never the account."
+ },
+ "sharing": {
+ "enum": [
+ "private",
+ "link",
+ "gallery"
+ ]
+ },
+ "look": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "component",
+ "label"
+ ],
+ "properties": {
+ "component": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,63}$",
+ "description": "An OpenVibe.Design component name (design.component@1), e.g. button or meter. A name the page does not have is refused, never fetched."
+ },
+ "label": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "The words on the element, e.g. Make it dance."
+ },
+ "icon": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,31}$",
+ "description": "A glyph name from the design icon set."
+ },
+ "props": {
+ "type": "object",
+ "maxProperties": 20,
+ "propertyNames": {
+ "pattern": "^[a-z][a-zA-Z0-9]{0,31}$"
+ },
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "maxLength": 200
+ }
+ ]
+ },
+ "description": "The design component's own props (the sliders the editor shows). Numbers, strings and booleans only; no nesting."
+ },
+ "style": {
+ "$ref": "#/$defs/style"
+ }
+ }
+ },
+ "recipe": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "triggers",
+ "actions"
+ ],
+ "properties": {
+ "triggers": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 8,
+ "items": {
+ "$ref": "trigger.v1.json"
+ }
+ },
+ "conditions": {
+ "$ref": "#/$defs/conditions"
+ },
+ "actions": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 16,
+ "items": {
+ "$ref": "action.v1.json"
+ }
+ }
+ }
+ },
+ "capabilities": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "$ref": "action.v1.json#/$defs/capability"
+ },
+ "description": "The action kinds this element may request. Every action in the recipe must be declared here; the host refuses the rest."
+ },
+ "initial_state": {
+ "$ref": "element-state.v1.json#/$defs/values",
+ "description": "The element's state before anything runs; the counter starts at 0, never at a value the element did not declare."
+ },
+ "mod": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "version",
+ "sha256"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$"
+ },
+ "sha256": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{64}$"
+ },
+ "capabilities": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "$ref": "action.v1.json#/$defs/capability"
+ },
+ "description": "The subset of the element's capabilities the module itself may request; omitted means the element's declared capabilities, and a product may grant fewer."
+ }
+ },
+ "description": "A code module for an element whose recipe is not enough (stage 3). It runs sandboxed; the reference is data, and the host checks every request the module makes against the element's capabilities."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "$defs": {
+ "style": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "tint": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,39}$",
+ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."
+ },
+ "shape": {
+ "enum": [
+ "round",
+ "pill",
+ "square"
+ ]
+ },
+ "opacity": {
+ "type": "number",
+ "minimum": 0.4,
+ "maximum": 1
+ },
+ "show_label": {
+ "type": "boolean"
+ }
+ }
+ },
+ "conditions": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "role": {
+ "enum": [
+ "everyone",
+ "follower",
+ "subscriber",
+ "vip",
+ "operator",
+ "owner"
+ ],
+ "description": "Who may fire it. A higher role admits the lower ones where the product says so (an operator is admitted to subscriber-only); owner is the channel's owner alone. Default everyone."
+ },
+ "cost": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "points"
+ ],
+ "properties": {
+ "points": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1000000
+ }
+ },
+ "description": "Channel points the presser spends, from Live's points. Stage 1 has no other currency, and an element never charges money."
+ },
+ "cooldown": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "per_person_seconds": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 86400
+ },
+ "overall_seconds": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 86400
+ }
+ },
+ "description": "How long the same person must wait, and how long everybody must wait, before it can fire again."
+ },
+ "daily_cap": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "per_person": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10000
+ },
+ "overall": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1000000
+ }
+ },
+ "description": "The most times it may fire in a UTC day, per person and for everyone."
+ },
+ "live_only": {
+ "type": "boolean",
+ "description": "When true the element fires only while the channel is live."
+ },
+ "robot_turn_only": {
+ "type": "boolean",
+ "description": "When true a robot action is requested only while the presser has the robot's turn; Bot still decides."
+ },
+ "chance": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "one_in"
+ ],
+ "properties": {
+ "one_in": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1000000
+ }
+ },
+ "description": "Fire only one time in N. The host draws the lot, so a refused draw costs nothing."
+ }
+ }
+ }
+ }
+ }
+ },
+ "ui.element-state": {
+ "version": "1.0.0",
+ "owner": "contracts",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/element-state.v1.json",
+ "title": "UiElementState",
+ "description": "ui.element-state@1: the small store an Element keeps for itself (counters, scores, lists, a wheel's entries), as Live broadcasts it to everyone watching and as a read returns it. CHANNEL and ELEMENT_ID name the element instance, REVISION grows by one with each accepted change so a late frame never overwrites a newer one, and VALUES is the store itself: at most 32 keys, each a boolean, a number, a short string or a list of those. It is state, not code: no objects, no markup, no URL and no account data, which keeps a shared element's state safe to render for every viewer.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "channel",
+ "element_id",
+ "revision",
+ "values",
+ "updated_at"
+ ],
+ "properties": {
+ "channel": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$",
+ "description": "The Live channel's username (its slug)."
+ },
+ "element_id": {
+ "type": "string",
+ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "values": {
+ "$ref": "#/$defs/values"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "$defs": {
+ "values": {
+ "type": "object",
+ "maxProperties": 32,
+ "propertyNames": {
+ "pattern": "^[a-z][a-z0-9_]{0,31}$"
+ },
+ "additionalProperties": {
+ "$ref": "#/$defs/value"
+ },
+ "description": "The element's own store. A key is lower-case and 32 characters at most; a value is a boolean, a number, a string of 200 characters at most or a list of those."
+ },
+ "value": {
+ "oneOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "maxLength": 200
+ },
+ {
+ "type": "array",
+ "maxItems": 64,
+ "items": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "maxLength": 200
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ui.trigger": {
+ "version": "1.0.0",
+ "owner": "contracts",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ui/trigger.v1.json",
+ "title": "UiTrigger",
+ "description": "ui.trigger@1: one trigger an Element's recipe listens for, from the fixed catalog. kind says what happened: a viewer pressing, holding or double-pressing the element, a key or gamepad binding, a chat command or text, a follow, a tip, an AI Moment, a Rundown topic, a timer or schedule, robot telemetry or an event, the stream starting or ending, a viewer-count crossing or another element's event. The fields a kind needs are the only fields it accepts. A trigger is data: it never names code or a URL, and a product may refuse a kind it does not offer yet. Stage 1 of Elements uses press and chat_command; the rest are declared here so a stage-2 element and the editor speak one language.",
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind"
+ ],
+ "properties": {
+ "kind": {
+ "const": "press"
+ }
+ },
+ "description": "The element itself was pressed. This is the Viewer Deck's button."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "ms"
+ ],
+ "properties": {
+ "kind": {
+ "const": "hold"
+ },
+ "ms": {
+ "type": "integer",
+ "minimum": 300,
+ "maximum": 10000
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind"
+ ],
+ "properties": {
+ "kind": {
+ "const": "double_press"
+ },
+ "within_ms": {
+ "type": "integer",
+ "minimum": 100,
+ "maximum": 2000,
+ "description": "The longest gap between the two presses; 400 by default."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "key"
+ ],
+ "properties": {
+ "kind": {
+ "const": "key"
+ },
+ "key": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"
+ },
+ "modifiers": {
+ "type": "array",
+ "maxItems": 4,
+ "uniqueItems": true,
+ "items": {
+ "enum": [
+ "ctrl",
+ "alt",
+ "shift",
+ "meta"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "button"
+ ],
+ "properties": {
+ "kind": {
+ "const": "gamepad"
+ },
+ "button": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ },
+ "index": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 7,
+ "description": "Which connected pad; omitted means any."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "command"
+ ],
+ "properties": {
+ "kind": {
+ "const": "chat_command"
+ },
+ "command": {
+ "type": "string",
+ "pattern": "^![a-z0-9_-]{1,32}$",
+ "description": "The command without arguments, lower case, e.g. !dance."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "words"
+ ],
+ "properties": {
+ "kind": {
+ "const": "chat_text"
+ },
+ "words": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 16,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40
+ }
+ },
+ "mode": {
+ "enum": [
+ "any",
+ "all"
+ ],
+ "description": "any (default) matches one of the words; all matches every one."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind"
+ ],
+ "properties": {
+ "kind": {
+ "const": "follow"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind"
+ ],
+ "properties": {
+ "kind": {
+ "const": "tip"
+ },
+ "min": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "The smallest tip, in Vibes, that fires the trigger; omitted means any tip."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind"
+ ],
+ "properties": {
+ "kind": {
+ "const": "ai_moment"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind"
+ ],
+ "properties": {
+ "kind": {
+ "const": "rundown_topic"
+ },
+ "words": {
+ "type": "array",
+ "maxItems": 8,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40
+ },
+ "description": "Optional words the topic has to mention; omitted means any topic."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "every_seconds"
+ ],
+ "properties": {
+ "kind": {
+ "const": "timer"
+ },
+ "every_seconds": {
+ "type": "integer",
+ "minimum": 5,
+ "maximum": 86400
+ },
+ "start": {
+ "enum": [
+ "live",
+ "always"
+ ],
+ "description": "live (default) counts only while the channel is live; always counts from the element being active."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "cron",
+ "timezone"
+ ],
+ "properties": {
+ "kind": {
+ "const": "schedule"
+ },
+ "cron": {
+ "type": "string",
+ "pattern": "^[0-9*/, -]{9,80}$",
+ "description": "A five-field cron in the person's timezone."
+ },
+ "timezone": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "metric",
+ "op",
+ "value"
+ ],
+ "properties": {
+ "kind": {
+ "const": "robot_telemetry"
+ },
+ "metric": {
+ "enum": [
+ "battery_percent",
+ "latency_ms",
+ "stuck",
+ "temperature_c"
+ ]
+ },
+ "op": {
+ "enum": [
+ "lt",
+ "gt"
+ ]
+ },
+ "value": {
+ "type": "number"
+ },
+ "for_seconds": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 3600,
+ "description": "How long the condition must hold; 0 (default) means at once."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "event"
+ ],
+ "properties": {
+ "kind": {
+ "const": "robot_event"
+ },
+ "event": {
+ "enum": [
+ "estop",
+ "stopped",
+ "disconnected",
+ "command_refused",
+ "button"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind"
+ ],
+ "properties": {
+ "kind": {
+ "const": "stream_start"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind"
+ ],
+ "properties": {
+ "kind": {
+ "const": "stream_end"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "op",
+ "count"
+ ],
+ "properties": {
+ "kind": {
+ "const": "viewer_count"
+ },
+ "op": {
+ "enum": [
+ "above",
+ "below"
+ ]
+ },
+ "count": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100000000
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "element_id"
+ ],
+ "properties": {
+ "kind": {
+ "const": "element_event"
+ },
+ "element_id": {
+ "type": "string",
+ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "event": {
+ "enum": [
+ "pressed",
+ "state",
+ "shown"
+ ],
+ "description": "pressed (default) fires after that element ran; state and shown fire when Live broadcasts them."
+ }
+ }
+ }
+ ]
+ }
+ },
+ "live.element.pressed": {
+ "version": "1.0.0",
+ "owner": "live",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/live.element.pressed.v1.json",
+ "title": "LiveElementPressedPayload",
+ "description": "live.element.pressed v1 (OpenVibe.Live server/elements): a viewer's element press was accepted and ran. It carries the public facts the stream should see: the channel, the element's id and name, the presser's public name and subject, the trigger that fired, the channel points it cost and the element-state revision it left behind. It never carries the presser's email, address, account balance or anything private; a refused press does not produce it. Envelope: subject { type: element, id: el_… }, actor the presser's subject, visibility public, priority low. Also written to the channel's AI timeline as a moment, so the stream's own timeline can cite 'chat made the robot dance'.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "channel",
+ "element_id",
+ "element_name",
+ "presser",
+ "presser_name",
+ "trigger",
+ "at"
+ ],
+ "properties": {
+ "channel": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "username",
+ "display_name"
+ ],
+ "properties": {
+ "username": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$"
+ },
+ "display_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ }
+ },
+ "description": "The public channel facts (the channel page is public already)."
+ },
+ "element_id": {
+ "type": "string",
+ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "element_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "presser": {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ "presser_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120,
+ "description": "The display name the stream shows, never an email or an account id."
+ },
+ "trigger": {
+ "$ref": "../../ui/trigger.v1.json"
+ },
+ "points_spent": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Channel points the press cost; 0 when it was free."
+ },
+ "state_revision": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "The element-state revision after the actions ran; 0 when the element has no state yet."
+ },
+ "at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "live.element.state": {
+ "version": "1.0.0",
+ "owner": "live",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/live.element.state.v1.json",
+ "title": "LiveElementStatePayload",
+ "description": "live.element.state v1 (OpenVibe.Live server/elements): an element's own store changed, broadcast to everyone watching through Live's existing events path. Carries the channel's public facts and the same store as ui.element-state@1: the element instance, a revision that grows by one per accepted change, the values themselves and when. It is public state (a counter, a score, a wheel's entries), never the presser's data: a value may not be an object, a markup string or a URL, and the payload carries no account.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "channel",
+ "element_id",
+ "revision",
+ "values",
+ "updated_at"
+ ],
+ "properties": {
+ "channel": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "username",
+ "display_name"
+ ],
+ "properties": {
+ "username": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$"
+ },
+ "display_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ }
+ }
+ },
+ "element_id": {
+ "type": "string",
+ "pattern": "^el_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "values": {
+ "$ref": "../../ui/element-state.v1.json#/$defs/values"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "ai.deep-dive": {
+ "version": "1.0.0",
+ "owner": "ai",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/ai/deep-dive.v1.json",
+ "title": "AiDeepDive",
+ "description": "A deep dive: expert design research run by OpenVibe.AI (workflow design.deep_dive) for OpenVibe.Exchange's /deep-dive page and, through the API, OpenVibe.Design. One document holds the question and depth (the input), the seven steps, every source read, every capture (a private Media object per viewport and theme, with its DOM facts and Expert extras), the per-site analysis, the synthesis across sites, the recommendations checked against OpenVibe's own rules, and the receipt of every model call and capture. Every claim carries its evidence: the site, the capture or crop, the URL and the date it was read. Nothing is copied: captures are private to the run's owner and the report shows them as attributed commentary.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "schema",
+ "run_id",
+ "status",
+ "input",
+ "steps",
+ "sources",
+ "captures",
+ "analysis",
+ "synthesis",
+ "recommendations",
+ "receipt",
+ "gaps",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The run id; a deep dive is a run (GET /api/v1/deep-dives/{id})."
+ },
+ "schema": {
+ "const": "ai.deep-dive@1"
+ },
+ "run_id": {
+ "type": "string",
+ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "status": {
+ "enum": [
+ "queued",
+ "running",
+ "choosing",
+ "succeeded",
+ "failed",
+ "cancelled"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "question",
+ "depth",
+ "max_sites"
+ ],
+ "properties": {
+ "question": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "depth": {
+ "enum": [
+ "quick",
+ "thorough",
+ "expert"
+ ]
+ },
+ "max_sites": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 30
+ },
+ "sites": {
+ "type": "array",
+ "maxItems": 30,
+ "items": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "description": "Sites the person named when starting the run."
+ }
+ }
+ },
+ "steps": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "key",
+ "status"
+ ],
+ "properties": {
+ "key": {
+ "enum": [
+ "plan",
+ "search",
+ "choose",
+ "capture",
+ "analyse",
+ "synthesise",
+ "report"
+ ]
+ },
+ "status": {
+ "enum": [
+ "pending",
+ "running",
+ "done",
+ "skipped",
+ "failed"
+ ]
+ },
+ "detail": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "finished_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "sources": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "kind",
+ "title",
+ "url",
+ "read_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "kind": {
+ "enum": [
+ "web",
+ "picture",
+ "icon",
+ "project",
+ "rules",
+ "component",
+ "named"
+ ]
+ },
+ "title": {
+ "type": "string",
+ "maxLength": 400
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "publisher": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200
+ },
+ "engine": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 80
+ },
+ "snippet": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "license": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 120
+ },
+ "read_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "captures": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "source_id",
+ "url",
+ "captured_at",
+ "viewport",
+ "theme",
+ "full_page",
+ "crops"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^cap_[A-Za-z0-9_-]{1,40}$"
+ },
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "final_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 400
+ },
+ "captured_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "viewport": {
+ "enum": [
+ 390,
+ 1280
+ ]
+ },
+ "theme": {
+ "enum": [
+ "light",
+ "dark"
+ ]
+ },
+ "full_page": {
+ "type": "boolean"
+ },
+ "media_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "width": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "height": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "crops": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "media_id",
+ "y",
+ "height"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^crop_[A-Za-z0-9_-]{1,40}$"
+ },
+ "media_id": {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "y": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "height": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ "dom": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "extras": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "skip": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200,
+ "description": "Why the capture did not happen: robots_disallowed, not_public, no_chrome, timeout, …"
+ },
+ "cached": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "analysis": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "source_id",
+ "url",
+ "summary",
+ "strengths",
+ "weaknesses",
+ "evidence"
+ ],
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "summary": {
+ "type": "string",
+ "maxLength": 4000
+ },
+ "first_impression": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "hierarchy": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "density": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "type": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "colour": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "motion": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "words": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "strengths": {
+ "type": "array",
+ "maxItems": 12,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ },
+ "weaknesses": {
+ "type": "array",
+ "maxItems": 12,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ },
+ "accessibility": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "speed": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ }
+ }
+ }
+ },
+ "synthesis": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "patterns",
+ "comparison",
+ "best",
+ "mistakes",
+ "gap",
+ "reference_tokens",
+ "do",
+ "dont",
+ "accessibility",
+ "speed"
+ ],
+ "properties": {
+ "patterns": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "what",
+ "sites",
+ "evidence"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "maxLength": 160
+ },
+ "what": {
+ "type": "string",
+ "maxLength": 1200
+ },
+ "sites": {
+ "type": "array",
+ "maxItems": 30,
+ "items": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ }
+ },
+ "usage_count": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 30
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ }
+ }
+ }
+ },
+ "comparison": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "columns",
+ "rows"
+ ],
+ "properties": {
+ "columns": {
+ "type": "array",
+ "maxItems": 12,
+ "items": {
+ "type": "string",
+ "maxLength": 120
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "maxItems": 12,
+ "items": {
+ "type": "string",
+ "maxLength": 1000
+ }
+ }
+ }
+ }
+ },
+ "best": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "source_id",
+ "what",
+ "evidence"
+ ],
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "what": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ }
+ }
+ },
+ "mistakes": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "what",
+ "evidence"
+ ],
+ "properties": {
+ "what": {
+ "type": "string",
+ "maxLength": 1200
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ }
+ }
+ }
+ },
+ "gap": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "reference_tokens": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "palettes",
+ "type_scales",
+ "spacing"
+ ],
+ "properties": {
+ "palettes": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "source_id",
+ "colours"
+ ],
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "colours": {
+ "type": "array",
+ "maxItems": 16,
+ "items": {
+ "type": "string",
+ "maxLength": 40
+ }
+ }
+ }
+ }
+ },
+ "type_scales": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "source_id",
+ "families",
+ "sizes"
+ ],
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "families": {
+ "type": "array",
+ "maxItems": 8,
+ "items": {
+ "type": "string",
+ "maxLength": 120
+ }
+ },
+ "sizes": {
+ "type": "array",
+ "maxItems": 16,
+ "items": {
+ "type": "string",
+ "maxLength": 40
+ }
+ }
+ }
+ }
+ },
+ "spacing": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "source_id",
+ "steps"
+ ],
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "steps": {
+ "type": "array",
+ "maxItems": 16,
+ "items": {
+ "type": "string",
+ "maxLength": 40
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "do": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ },
+ "dont": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ },
+ "accessibility": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ },
+ "speed": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "maxLength": 600
+ }
+ }
+ }
+ },
+ "recommendations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "what",
+ "why",
+ "rule",
+ "evidence"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^rec_[A-Za-z0-9_-]{1,40}$"
+ },
+ "what": {
+ "type": "string",
+ "maxLength": 1200
+ },
+ "why": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "rule": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "status",
+ "text"
+ ],
+ "properties": {
+ "id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 120
+ },
+ "status": {
+ "enum": [
+ "follows",
+ "would_change",
+ "none"
+ ]
+ },
+ "text": {
+ "type": "string",
+ "maxLength": 600
+ }
+ }
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ }
+ }
+ }
+ },
+ "receipt": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "price_usd",
+ "held_usd",
+ "cost_usd",
+ "models",
+ "captures_count",
+ "synthetic"
+ ],
+ "properties": {
+ "price_usd": {
+ "type": "number",
+ "minimum": 0
+ },
+ "held_usd": {
+ "type": "number",
+ "minimum": 0
+ },
+ "cost_usd": {
+ "type": "number",
+ "minimum": 0
+ },
+ "captures_count": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "synthetic": {
+ "type": "boolean"
+ },
+ "models": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "step",
+ "provider",
+ "model",
+ "tokens_in",
+ "tokens_out",
+ "cost_usd",
+ "ms"
+ ],
+ "properties": {
+ "step": {
+ "enum": [
+ "plan",
+ "choose",
+ "analyse",
+ "synthesise"
+ ]
+ },
+ "provider": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 80
+ },
+ "model": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200
+ },
+ "tokens_in": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "tokens_out": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "cost_usd": {
+ "type": "number",
+ "minimum": 0
+ },
+ "ms": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ }
+ }
+ },
+ "gaps": {
+ "type": "array",
+ "maxItems": 40,
+ "items": {
+ "type": "string",
+ "maxLength": 400
+ }
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "finished_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ },
+ "$defs": {
+ "evidence": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "claim",
+ "url",
+ "read_at"
+ ],
+ "properties": {
+ "claim": {
+ "type": "string",
+ "maxLength": 600
+ },
+ "source_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^src_[A-Za-z0-9_-]{1,40}$"
+ },
+ "capture_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^cap_[A-Za-z0-9_-]{1,40}$"
+ },
+ "crop_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^crop_[A-Za-z0-9_-]{1,40}$"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "read_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "ai.deep_dive.updated": {
+ "version": "1.0.0",
+ "owner": "ai",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/ai.deep_dive.updated.v1.json",
+ "title": "AiDeepDiveUpdatedPayload",
+ "description": "ai.deep_dive.updated v1 (OpenVibe.AI server/deep-dive): a deep dive's step changed or it ended. One event per step transition and one at the end, so a page (OpenVibe.Exchange's /deep-dive) can follow a run without polling, and OpenVibe.Design can attach a finished report. The payload carries the run and deep-dive ids, the step, the progress and the status; never the question's answer, a capture or a prompt. Envelope: subject { type: run, id: <run_id> }, visibility internal, priority low (important on failed), actor service:ai.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "deep_dive_id",
+ "run_id",
+ "status",
+ "step",
+ "progress",
+ "steps",
+ "depth",
+ "requester",
+ "on_behalf_of",
+ "url",
+ "error",
+ "occurred_at"
+ ],
+ "properties": {
+ "deep_dive_id": {
+ "type": "string",
+ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "run_id": {
+ "type": "string",
+ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "status": {
+ "enum": [
+ "queued",
+ "running",
+ "choosing",
+ "succeeded",
+ "failed",
+ "cancelled"
+ ]
+ },
+ "step": {
+ "enum": [
+ "plan",
+ "search",
+ "choose",
+ "capture",
+ "analyse",
+ "synthesise",
+ "report",
+ "done",
+ null
+ ]
+ },
+ "progress": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "steps": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "key",
+ "status"
+ ],
+ "properties": {
+ "key": {
+ "enum": [
+ "plan",
+ "search",
+ "choose",
+ "capture",
+ "analyse",
+ "synthesise",
+ "report"
+ ]
+ },
+ "status": {
+ "enum": [
+ "pending",
+ "running",
+ "done",
+ "skipped",
+ "failed"
+ ]
+ }
+ }
+ }
+ },
+ "depth": {
+ "enum": [
+ "quick",
+ "thorough",
+ "expert"
+ ]
+ },
+ "requester": {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ "on_behalf_of": {
+ "anyOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://"
+ },
+ "error": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "code",
+ "detail"
+ ],
+ "properties": {
+ "code": {
+ "type": "string",
+ "maxLength": 80
+ },
+ "detail": {
+ "type": "string",
+ "maxLength": 500
+ }
+ }
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "occurred_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
}
}
}
}
}
}
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Actor API",
"title": "OpenVibe.Actor API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.AI API",
"title": "OpenVibe.AI API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3986,6 +3986,62 @@
@@ -3986,6 +3986,62 @@
}
}
}
}
},
},
+ "/api/v1/ui/layout/suggest": {
+ "post": {
+ "operationId": "post_api_v1_ui_layout_suggest",
+ "summary": "Turn a person's words into a ui.layout@1 for one surface, using only the parts its ui.surface@1 catalog has.",
+ "description": "**ai.layout.suggest** (first-party): Turn a person's words into a ui.layout@1 for one surface, using only the parts its ui.surface@1 catalog has. The answer is checked by openvibe-sdk/ui-layout against the same catalog; an invalid answer gets one retry with the validator's reasons, then the task says plainly that it could not do it. The suggestion is previewed, never applied. Runs on the cheap structured-output route with the per-person limits every AI run uses, and its receipt names the model. POST /api/v1/ui/layout/suggest with a service token; OpenVibe.Network calls it for the signed-in person and names them in X-OV-Subject.",
+ "tags": [
+ "ai.layout"
+ ],
+ "x-openvibe-capabilities": [
+ "ai.layout.suggest"
+ ],
+ "x-openvibe-visibility": [
+ "first-party"
+ ],
+ "x-openvibe-quota-class": "ai-run",
+ "security": [
+ {
+ "openvibeToken": [
+ "ai.layout.suggest"
+ ]
+ }
+ ],
+ "requestBody": {
+ "required": false,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ui.layout-request.v1"
+ }
+ }
+ }
+ },
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ui.layout-suggestion.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/v1/usage": {
"/api/v1/usage": {
"get": {
"get": {
"operationId": "get_api_v1_usage",
"operationId": "get_api_v1_usage",
@@ -10110,6 +10166,750 @@
@@ -10110,6 +10166,750 @@
"description": "AgentSubject: an agent (OpenVibe.Actor, or a developer app's agent) acting under grants its owner delegated (roadmap WS-Z2). In an event envelope, on_behalf_of names the owner."
"description": "AgentSubject: an agent (OpenVibe.Actor, or a developer app's agent) acting under grants its owner delegated (roadmap WS-Z2). In an event envelope, on_behalf_of names the owner."
}
}
]
]
+ },
+ "ui.layout-request.v1": {
+ "title": "UiLayoutRequest",
+ "description": "ui.layout-request@1: what an editor sends OpenVibe.AI's ui.layout.suggest task, and what Network forwards for a signed-in person. It is the surface's catalog, the layout shown now (or null for the default one) and the words the person typed (max 500 characters), for one device class. The catalog is the only power the answer can use; the current layout is the starting arrangement; the words never contain markup that becomes part of it.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "catalog",
+ "layout",
+ "words",
+ "device"
+ ],
+ "properties": {
+ "catalog": {
+ "$ref": "#/components/schemas/ui.surface.v1"
+ },
+ "layout": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ui.layout.v1"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The arrangement shown now, or null when the person is looking at the default."
+ },
+ "words": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500,
+ "description": "The person's own description, e.g. \"put Stop on the left and make it huge\"."
+ },
+ "device": {
+ "enum": [
+ "phone",
+ "tablet",
+ "computer"
+ ]
+ }
+ }
+ },
+ "ui.layout-suggestion.v1": {
+ "title": "UiLayoutSuggestion",
+ "description": "ui.layout-suggestion@1: OpenVibe.AI's answer to ui.layout.suggest. ok true carries the layout, already checked by openvibe-sdk/ui-layout against the catalog sent with the request, and a one-sentence summary of what changed plus the model's reasons. ok false is an honest refusal: no layout, one sentence saying what it could not do, and the validator's reasons in plain words (for example \"Stop has to stay visible.\"). A receipt names the model that answered, with the run and the route. The suggestion is previewed, never applied silently; Network or the product stores it only after the person accepts it.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "ok",
+ "layout",
+ "summary",
+ "reasons"
+ ],
+ "properties": {
+ "ok": {
+ "type": "boolean"
+ },
+ "layout": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ui.layout.v1"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The validated layout when ok is true; null when it is false."
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300,
+ "description": "One sentence saying what changed, or what could not be done."
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ },
+ "description": "Why this arrangement, or the validator's reasons for refusing one."
+ },
+ "receipt": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "model"
+ ],
+ "properties": {
+ "model": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "provider": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "route": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "run_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "cost_usd": {
+ "type": "number",
+ "minimum": 0
+ }
+ },
+ "description": "Which model answered: the same receipt every AI run carries, at the task's own shape."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "ok": {
+ "const": true
+ }
+ },
+ "required": [
+ "ok"
+ ]
+ },
+ "then": {
+ "properties": {
+ "layout": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "ok": {
+ "const": false
+ }
+ },
+ "required": [
+ "ok"
+ ]
+ },
+ "then": {
+ "properties": {
+ "layout": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "ui.layout.v1": {
+ "title": "UiLayout",
+ "description": "ui.layout@1: one person's arrangement of one surface on one device class. A layout names the surface and its catalog version, the device, its own name, its author and its sharing (private, link or gallery), and one entry per part it places. A part is visible or not, sits on the surface's grid (x, y, w, h in cells) or docks to an edge for an overlay, and carries a style (a theme token tint, a shape, opacity 0.4-1 and the label) and bindings. It is data only: no URL, no markup and no code may appear anywhere in it, and every part id, tint, shape, opacity, size and binding must be one the catalog allows. openvibe-sdk/ui-layout validates it before any server, editor or AI answer stores or shows it.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "surface",
+ "surface_version",
+ "device",
+ "name",
+ "author",
+ "sharing",
+ "parts"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Assigned by OpenVibe.Network when the layout is stored; absent in a draft, a share answer or an AI suggestion."
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Assigned by the store; every change to a stored layout moves it by one."
+ },
+ "active": {
+ "type": "boolean",
+ "description": "Set by OpenVibe.Network: the one layout a product draws for this surface on this device class. A draft, a share answer and an AI suggestion carry no active field."
+ },
+ "surface": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",
+ "maxLength": 80
+ },
+ "surface_version": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "device": {
+ "enum": [
+ "phone",
+ "tablet",
+ "computer"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "The person's name for this layout; \"My phone controls\" is fine."
+ },
+ "author": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "subject": {
+ "$ref": "#/components/schemas/identity.subject-ref.v1"
+ }
+ },
+ "description": "The author's display name and, for a private or link layout, the subject. A share or gallery answer carries the display name only, never the account."
+ },
+ "sharing": {
+ "enum": [
+ "private",
+ "link",
+ "gallery"
+ ]
+ },
+ "parts": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 64,
+ "items": {
+ "$ref": "#/components/schemas/ui.layout.v1/$defs/part"
+ }
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "$defs": {
+ "place": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "x",
+ "y",
+ "w",
+ "h"
+ ],
+ "properties": {
+ "x": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 23
+ },
+ "y": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100
+ },
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ }
+ },
+ "description": "The part's cell on the surface grid."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "dock",
+ "order",
+ "w",
+ "h"
+ ],
+ "properties": {
+ "dock": {
+ "enum": [
+ "left",
+ "right",
+ "top",
+ "bottom"
+ ]
+ },
+ "order": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 63
+ },
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ }
+ },
+ "description": "For an overlay: the edge the part docks to, its order along that edge, and its size in cells."
+ }
+ ]
+ },
+ "style": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "tint",
+ "shape",
+ "opacity",
+ "label"
+ ],
+ "properties": {
+ "tint": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,39}$",
+ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."
+ },
+ "shape": {
+ "enum": [
+ "round",
+ "pill",
+ "square"
+ ]
+ },
+ "opacity": {
+ "type": "number",
+ "minimum": 0.4,
+ "maximum": 1
+ },
+ "label": {
+ "type": "boolean"
+ }
+ }
+ },
+ "bindings": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "keys",
+ "gamepad"
+ ],
+ "properties": {
+ "keys": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"
+ }
+ },
+ "gamepad": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ }
+ }
+ }
+ },
+ "part": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "visible",
+ "place",
+ "style",
+ "bindings"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ },
+ "visible": {
+ "type": "boolean"
+ },
+ "place": {
+ "$ref": "#/components/schemas/ui.layout.v1/$defs/place"
+ },
+ "style": {
+ "$ref": "#/components/schemas/ui.layout.v1/$defs/style"
+ },
+ "bindings": {
+ "$ref": "#/components/schemas/ui.layout.v1/$defs/bindings"
+ }
+ }
+ }
+ }
+ },
+ "ui.surface.v1": {
+ "title": "UiSurface",
+ "description": "ui.surface@1: the catalog a product publishes for one surface people may arrange (bot.controls, bot.overlay, live.channel, dock, chat.composer, navbar.shortcuts). It names the surface, its device classes and grid, the theme tokens its styles draw from, and every part a layout may place. A part says what it is (stick, button, slider, readout, video, list), whether it is required or pressed, its minimum cells and the minimum pixels the product guarantees (44 px for anything pressed, larger for Stop), the sizes and styles it allows, the bindings it accepts, and the parts it must never cover. A layout is validated against this catalog (openvibe-sdk/ui-layout); a layout can arrange only what the catalog has and can never add a power. The catalog is data: a layout or an AI answer never carries code or a URL.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "version",
+ "name",
+ "devices",
+ "grid",
+ "theme_tokens",
+ "parts"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",
+ "maxLength": 80,
+ "description": "The surface id: <product>.<surface>, e.g. bot.controls."
+ },
+ "version": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The catalog version. A layout names the version it was made against."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "What the surface is called where a person sees it."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 500
+ },
+ "devices": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 3,
+ "uniqueItems": true,
+ "items": {
+ "enum": [
+ "phone",
+ "tablet",
+ "computer"
+ ]
+ },
+ "description": "The device classes this surface offers."
+ },
+ "grid": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "columns",
+ "cell_px"
+ ],
+ "properties": {
+ "columns": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "phone",
+ "tablet",
+ "computer"
+ ],
+ "description": "The grid's columns per device class: 4 on a phone and 12 on a computer in the design; a product may choose otherwise within these bounds.",
+ "properties": {
+ "phone": {
+ "type": "integer",
+ "minimum": 2,
+ "maximum": 8
+ },
+ "tablet": {
+ "type": "integer",
+ "minimum": 2,
+ "maximum": 16
+ },
+ "computer": {
+ "type": "integer",
+ "minimum": 2,
+ "maximum": 24
+ }
+ }
+ },
+ "cell_px": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "phone",
+ "tablet",
+ "computer"
+ ],
+ "description": "The smallest a cell is rendered at that device class, in CSS pixels, at the narrowest supported width. The validator multiplies a part's cells by this to check the 44 px rule.",
+ "properties": {
+ "phone": {
+ "type": "integer",
+ "minimum": 24,
+ "maximum": 200
+ },
+ "tablet": {
+ "type": "integer",
+ "minimum": 24,
+ "maximum": 200
+ },
+ "computer": {
+ "type": "integer",
+ "minimum": 24,
+ "maximum": 200
+ }
+ }
+ }
+ }
+ },
+ "theme_tokens": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,39}$"
+ },
+ "description": "The theme tokens a tint may name (accent, surface, danger, ...). Never a colour value: the theme decides what the token is."
+ },
+ "parts": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 64,
+ "items": {
+ "$ref": "#/components/schemas/ui.surface.v1/$defs/part"
+ }
+ }
+ },
+ "$defs": {
+ "part": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "name",
+ "kind",
+ "required",
+ "pressed",
+ "min",
+ "styles",
+ "bindings"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$",
+ "description": "Stable part id inside this catalog, e.g. stop, stick, battery."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "kind": {
+ "enum": [
+ "stick",
+ "button",
+ "slider",
+ "readout",
+ "video",
+ "list"
+ ]
+ },
+ "required": {
+ "type": "boolean",
+ "description": "A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused."
+ },
+ "pressed": {
+ "type": "boolean",
+ "description": "The part sends a command when pressed or moved; it must render at least 44 px on every side."
+ },
+ "min": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "w",
+ "h"
+ ],
+ "description": "The smallest size a layout may give the part: w and h in grid cells, and px when the product guarantees more than 44 px (Stop).",
+ "properties": {
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "px": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 400
+ }
+ }
+ },
+ "sizes": {
+ "type": "array",
+ "maxItems": 12,
+ "uniqueItems": true,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "w",
+ "h"
+ ],
+ "properties": {
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ }
+ }
+ },
+ "description": "Sizes the editor may offer as presets. A layout is not limited to these: it is limited to min and to the grid."
+ },
+ "styles": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "tints",
+ "shapes"
+ ],
+ "properties": {
+ "tints": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 16,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,39}$"
+ },
+ "description": "The theme tokens this part may be tinted with; every one is also in the surface's theme_tokens."
+ },
+ "shapes": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 3,
+ "uniqueItems": true,
+ "items": {
+ "enum": [
+ "round",
+ "pill",
+ "square"
+ ]
+ }
+ },
+ "opacity": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "min",
+ "max"
+ ],
+ "properties": {
+ "min": {
+ "type": "number",
+ "minimum": 0.4,
+ "maximum": 1
+ },
+ "max": {
+ "type": "number",
+ "minimum": 0.4,
+ "maximum": 1
+ }
+ },
+ "description": "The opacity range this part allows; the design's range is 0.4 to 1."
+ },
+ "label": {
+ "type": "boolean",
+ "description": "Whether the editor may show the part's label on it."
+ }
+ }
+ },
+ "bindings": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "keys",
+ "gamepad"
+ ],
+ "properties": {
+ "keys": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"
+ }
+ },
+ "gamepad": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ }
+ }
+ },
+ "description": "The bindings the product accepts for this part. A layout may pick a subset, never add one."
+ },
+ "never_covers": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ },
+ "description": "The parts this part must never cover, in addition to the surface-wide rule that nothing covers a required part."
+ }
+ }
+ }
+ }
}
}
},
},
"securitySchemes": {
"securitySchemes": {
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Bench API",
"title": "OpenVibe.Bench API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Billing API",
"title": "OpenVibe.Billing API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Blog API",
"title": "OpenVibe.Blog API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Bot API",
"title": "OpenVibe.Bot API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Chat API",
"title": "OpenVibe.Chat API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Codes API",
"title": "OpenVibe.Codes API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Community API",
"title": "OpenVibe.Community API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe Connect API",
"title": "OpenVibe Connect API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe Connect as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe Connect as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Coupons API",
"title": "OpenVibe.Coupons API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Deals API",
"title": "OpenVibe.Deals API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Design API",
"title": "OpenVibe.Design API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Design as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Design as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Domains API",
"title": "OpenVibe.Domains API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Events API",
"title": "OpenVibe.Events API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Exchange API",
"title": "OpenVibe.Exchange API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Exchange as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Exchange as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Food API",
"title": "OpenVibe.Food API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Games API",
"title": "OpenVibe.Games API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Git API",
"title": "OpenVibe.Git API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Git as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Git as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Host API",
"title": "OpenVibe.Host API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -12,8 +12,8 @@
@@ -12,8 +12,8 @@
"service": "ai",
"service": "ai",
"name": "OpenVibe.AI",
"name": "OpenVibe.AI",
"origin": "https://ai.openvibe.services",
"origin": "https://ai.openvibe.services",
- "operations": 71,
- "capabilities": 18
+ "operations": 72,
+ "capabilities": 19
},
},
{
{
"service": "bench",
"service": "bench",
@@ -180,8 +180,8 @@
@@ -180,8 +180,8 @@
"service": "network",
"service": "network",
"name": "OpenVibe.Network",
"name": "OpenVibe.Network",
"origin": "https://openvibe.network",
"origin": "https://openvibe.network",
- "operations": 73,
- "capabilities": 43
+ "operations": 74,
+ "capabilities": 44
},
},
{
{
"service": "news",
"service": "news",
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Inventory API",
"title": "OpenVibe.Inventory API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Live API",
"title": "OpenVibe.Live API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Mail API",
"title": "OpenVibe.Mail API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Mail as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Mail as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.MediaHub API",
"title": "OpenVibe.MediaHub API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Media API",
"title": "OpenVibe.Media API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Network API",
"title": "OpenVibe.Network API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -2057,6 +2057,70 @@
@@ -2057,6 +2057,70 @@
}
}
}
}
},
},
+ "/api/v1/people/{subject}/layouts/{surface}": {
+ "get": {
+ "operationId": "get_api_v1_people_subject_layouts_surface",
+ "summary": "Read the active ui.layout@1 one person made for a product's own surface, with that surface's device class.",
+ "description": "**network.layout.read** (first-party): Read the active ui.layout@1 one person made for a product's own surface, with that surface's device class. A first-party service presents a service token for openvibe.network and names the person with X-OV-Subject (which must be the person in the path); it reads one layout and nothing else of their account, and never their saved or gallery layouts. The person's own token uses GET /api/v1/me/layouts/:surface instead, which reads and writes only their own rows.",
+ "tags": [
+ "network.layout"
+ ],
+ "x-openvibe-capabilities": [
+ "network.layout.read"
+ ],
+ "x-openvibe-visibility": [
+ "first-party"
+ ],
+ "x-openvibe-quota-class": "standard",
+ "security": [
+ {
+ "openvibeToken": [
+ "network.layout.read"
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "subject",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "surface",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ui.layout.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/v1/resources": {
"/api/v1/resources": {
"get": {
"get": {
"operationId": "get_api_v1_resources",
"operationId": "get_api_v1_resources",
@@ -9635,6 +9699,260 @@
@@ -9635,6 +9699,260 @@
"format": "date-time"
"format": "date-time"
}
}
}
}
+ },
+ "ui.layout.v1": {
+ "title": "UiLayout",
+ "description": "ui.layout@1: one person's arrangement of one surface on one device class. A layout names the surface and its catalog version, the device, its own name, its author and its sharing (private, link or gallery), and one entry per part it places. A part is visible or not, sits on the surface's grid (x, y, w, h in cells) or docks to an edge for an overlay, and carries a style (a theme token tint, a shape, opacity 0.4-1 and the label) and bindings. It is data only: no URL, no markup and no code may appear anywhere in it, and every part id, tint, shape, opacity, size and binding must be one the catalog allows. openvibe-sdk/ui-layout validates it before any server, editor or AI answer stores or shows it.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "surface",
+ "surface_version",
+ "device",
+ "name",
+ "author",
+ "sharing",
+ "parts"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^lay_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Assigned by OpenVibe.Network when the layout is stored; absent in a draft, a share answer or an AI suggestion."
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Assigned by the store; every change to a stored layout moves it by one."
+ },
+ "active": {
+ "type": "boolean",
+ "description": "Set by OpenVibe.Network: the one layout a product draws for this surface on this device class. A draft, a share answer and an AI suggestion carry no active field."
+ },
+ "surface": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)+$",
+ "maxLength": 80
+ },
+ "surface_version": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "device": {
+ "enum": [
+ "phone",
+ "tablet",
+ "computer"
+ ]
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "The person's name for this layout; \"My phone controls\" is fine."
+ },
+ "author": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "subject": {
+ "$ref": "#/components/schemas/identity.subject-ref.v1"
+ }
+ },
+ "description": "The author's display name and, for a private or link layout, the subject. A share or gallery answer carries the display name only, never the account."
+ },
+ "sharing": {
+ "enum": [
+ "private",
+ "link",
+ "gallery"
+ ]
+ },
+ "parts": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 64,
+ "items": {
+ "$ref": "#/components/schemas/ui.layout.v1/$defs/part"
+ }
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "$defs": {
+ "place": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "x",
+ "y",
+ "w",
+ "h"
+ ],
+ "properties": {
+ "x": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 23
+ },
+ "y": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100
+ },
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ }
+ },
+ "description": "The part's cell on the surface grid."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "dock",
+ "order",
+ "w",
+ "h"
+ ],
+ "properties": {
+ "dock": {
+ "enum": [
+ "left",
+ "right",
+ "top",
+ "bottom"
+ ]
+ },
+ "order": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 63
+ },
+ "w": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 24
+ }
+ },
+ "description": "For an overlay: the edge the part docks to, its order along that edge, and its size in cells."
+ }
+ ]
+ },
+ "style": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "tint",
+ "shape",
+ "opacity",
+ "label"
+ ],
+ "properties": {
+ "tint": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,39}$",
+ "description": "A theme token, never a colour: the theme decides what it is, so contrast follows the theme."
+ },
+ "shape": {
+ "enum": [
+ "round",
+ "pill",
+ "square"
+ ]
+ },
+ "opacity": {
+ "type": "number",
+ "minimum": 0.4,
+ "maximum": 1
+ },
+ "label": {
+ "type": "boolean"
+ }
+ }
+ },
+ "bindings": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "keys",
+ "gamepad"
+ ],
+ "properties": {
+ "keys": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9+_.-]{1,40}$"
+ }
+ },
+ "gamepad": {
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ }
+ }
+ }
+ },
+ "part": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "visible",
+ "place",
+ "style",
+ "bindings"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.-]{0,39}$"
+ },
+ "visible": {
+ "type": "boolean"
+ },
+ "place": {
+ "$ref": "#/components/schemas/ui.layout.v1/$defs/place"
+ },
+ "style": {
+ "$ref": "#/components/schemas/ui.layout.v1/$defs/style"
+ },
+ "bindings": {
+ "$ref": "#/components/schemas/ui.layout.v1/$defs/bindings"
+ }
+ }
+ }
+ }
}
}
},
},
"securitySchemes": {
"securitySchemes": {
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.News API",
"title": "OpenVibe.News API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenRestream API",
"title": "OpenRestream API",
- "version": "0.409.0",
- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Play API",
"title": "OpenVibe.Play API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Quest API",
"title": "OpenVibe.Quest API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Report API",
"title": "OpenVibe.Report API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Reviews API",
"title": "OpenVibe.Reviews API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Run API",
"title": "OpenVibe.Run API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Search API",
"title": "OpenVibe.Search API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Services API",
"title": "OpenVibe.Services API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Sources API",
"title": "OpenVibe.Sources API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Space API",
"title": "OpenVibe.Space API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tips API",
"title": "OpenVibe.Tips API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Today API",
"title": "OpenVibe.Today API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tools API",
"title": "OpenVibe.Tools API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Trade API",
"title": "OpenVibe.Trade API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.VIP API",
"title": "OpenVibe.VIP API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Vision API",
"title": "OpenVibe.Vision API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Watch API",
"title": "OpenVibe.Watch API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Wiki API",
"title": "OpenVibe.Wiki API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Work API",
"title": "OpenVibe.Work API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Zone API",
"title": "OpenVibe.Zone API",
- "version": "0.409.0",
- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.409.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.410.0",
+ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.410.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -338191,3 +338191,3156 @@ export interface NetworkAgentTaskUpdatedPayload {
@@ -338191,3 +338191,3156 @@ export interface NetworkAgentTaskUpdatedPayload {
status: "pending" | "running" | "completed" | "failed" | "cancelled";
status: "pending" | "running" | "completed" | "failed" | "cancelled";
at: string;
at: string;
}
}
+
+/** [email protected] (owner: contracts) */
+/**
+ * ui.surface@1: the catalog a product publishes for one surface people may arrange (bot.controls, bot.overlay, live.channel, dock, chat.composer, navbar.shortcuts). It names the surface, its device classes and grid, the theme tokens its styles draw from, and every part a layout may place. A part says what it is (stick, button, slider, readout, video, list), whether it is required or pressed, its minimum cells and the minimum pixels the product guarantees (44 px for anything pressed, larger for Stop), the sizes and styles it allows, the bindings it accepts, and the parts it must never cover. A layout is validated against this catalog (openvibe-sdk/ui-layout); a layout can arrange only what the catalog has and can never add a power. The catalog is data: a layout or an AI answer never carries code or a URL.
+ */
+export interface UiSurface {
+ /**
+ * The surface id: <product>.<surface>, e.g. bot.controls.
+ */
+ id: string;
+ /**
+ * The catalog version. A layout names the version it was made against.
+ */
+ version: number;
+ /**
+ * What the surface is called where a person sees it.
+ */
+ name: string;
+ description?: string;
+ /**
+ * The device classes this surface offers.
+ *
+ * @minItems 1
+ * @maxItems 3
+ */
+ devices:
+ | ["phone" | "tablet" | "computer"]
+ | ["phone" | "tablet" | "computer", "phone" | "tablet" | "computer"]
+ | ["phone" | "tablet" | "computer", "phone" | "tablet" | "computer", "phone" | "tablet" | "computer"];
+ grid: {
+ /**
+ * The grid's columns per device class: 4 on a phone and 12 on a computer in the design; a product may choose otherwise within these bounds.
+ */
+ columns: {
+ phone: number;
+ tablet: number;
+ computer: number;
+ };
+ /**
+ * The smallest a cell is rendered at that device class, in CSS pixels, at the narrowest supported width. The validator multiplies a part's cells by this to check the 44 px rule.
+ */
+ cell_px: {
+ phone: number;
+ tablet: number;
+ computer: number;
+ };
+ };
+ /**
+ * The theme tokens a tint may name (accent, surface, danger, ...). Never a colour value: the theme decides what the token is.
+ *
+ * @minItems 1
+ * @maxItems 64
+ */
+ theme_tokens: [string, ...string[]];
+ /**
+ * @minItems 1
+ * @maxItems 64
+ */
+ parts: [
+ {
+ /**
+ * Stable part id inside this catalog, e.g. stop, stick, battery.
+ */
+ id: string;
+ name: string;
+ kind: "stick" | "button" | "slider" | "readout" | "video" | "list";
+ /**
+ * A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused.
+ */
+ required: boolean;
+ /**
+ * The part sends a command when pressed or moved; it must render at least 44 px on every side.
+ */
+ pressed: boolean;
+ /**
+ * The smallest size a layout may give the part: w and h in grid cells, and px when the product guarantees more than 44 px (Stop).
+ */
+ min: {
+ w: number;
+ h: number;
+ px?: number;
+ };
+ /**
+ * Sizes the editor may offer as presets. A layout is not limited to these: it is limited to min and to the grid.
+ *
+ * @maxItems 12
+ */
+ sizes?:
+ | []
+ | [
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ];
+ styles: {
+ /**
+ * The theme tokens this part may be tinted with; every one is also in the surface's theme_tokens.
+ *
+ * @minItems 1
+ * @maxItems 16
+ */
+ tints:
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ /**
+ * @minItems 1
+ * @maxItems 3
+ */
+ shapes:
+ | ["round" | "pill" | "square"]
+ | ["round" | "pill" | "square", "round" | "pill" | "square"]
+ | ["round" | "pill" | "square", "round" | "pill" | "square", "round" | "pill" | "square"];
+ /**
+ * The opacity range this part allows; the design's range is 0.4 to 1.
+ */
+ opacity?: {
+ min: number;
+ max: number;
+ };
+ /**
+ * Whether the editor may show the part's label on it.
+ */
+ label?: boolean;
+ };
+ /**
+ * The bindings the product accepts for this part. A layout may pick a subset, never add one.
+ */
+ bindings: {
+ /**
+ * @maxItems 32
+ */
+ keys: string[];
+ /**
+ * @maxItems 32
+ */
+ gamepad: string[];
+ };
+ /**
+ * The parts this part must never cover, in addition to the surface-wide rule that nothing covers a required part.
+ *
+ * @maxItems 32
+ */
+ never_covers?: string[];
+ },
+ ...{
+ /**
+ * Stable part id inside this catalog, e.g. stop, stick, battery.
+ */
+ id: string;
+ name: string;
+ kind: "stick" | "button" | "slider" | "readout" | "video" | "list";
+ /**
+ * A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused.
+ */
+ required: boolean;
+ /**
+ * The part sends a command when pressed or moved; it must render at least 44 px on every side.
+ */
+ pressed: boolean;
+ /**
+ * The smallest size a layout may give the part: w and h in grid cells, and px when the product guarantees more than 44 px (Stop).
+ */
+ min: {
+ w: number;
+ h: number;
+ px?: number;
+ };
+ /**
+ * Sizes the editor may offer as presets. A layout is not limited to these: it is limited to min and to the grid.
+ *
+ * @maxItems 12
+ */
+ sizes?:
+ | []
+ | [
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ]
+ | [
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ },
+ {
+ w: number;
+ h: number;
+ }
+ ];
+ styles: {
+ /**
+ * The theme tokens this part may be tinted with; every one is also in the surface's theme_tokens.
+ *
+ * @minItems 1
+ * @maxItems 16
+ */
+ tints:
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ /**
+ * @minItems 1
+ * @maxItems 3
+ */
+ shapes:
+ | ["round" | "pill" | "square"]
+ | ["round" | "pill" | "square", "round" | "pill" | "square"]
+ | ["round" | "pill" | "square", "round" | "pill" | "square", "round" | "pill" | "square"];
+ /**
+ * The opacity range this part allows; the design's range is 0.4 to 1.
+ */
+ opacity?: {
+ min: number;
+ max: number;
+ };
+ /**
+ * Whether the editor may show the part's label on it.
+ */
+ label?: boolean;
+ };
+ /**
+ * The bindings the product accepts for this part. A layout may pick a subset, never add one.
+ */
+ bindings: {
+ /**
+ * @maxItems 32
+ */
+ keys: string[];
+ /**
+ * @maxItems 32
+ */
+ gamepad: string[];
+ };
+ /**
+ * The parts this part must never cover, in addition to the surface-wide rule that nothing covers a required part.
+ *
+ * @maxItems 32
+ */
+ never_covers?: string[];
+ }[]
+ ];
+}
+
+/** [email protected] (owner: contracts) */
+/**
+ * ui.layout@1: one person's arrangement of one surface on one device class. A layout names the surface and its catalog version, the device, its own name, its author and its sharing (private, link or gallery), and one entry per part it places. A part is visible or not, sits on the surface's grid (x, y, w, h in cells) or docks to an edge for an overlay, and carries a style (a theme token tint, a shape, opacity 0.4-1 and the label) and bindings. It is data only: no URL, no markup and no code may appear anywhere in it, and every part id, tint, shape, opacity, size and binding must be one the catalog allows. openvibe-sdk/ui-layout validates it before any server, editor or AI answer stores or shows it.
+ */
+export interface UiLayout {
+ /**
+ * Assigned by OpenVibe.Network when the layout is stored; absent in a draft, a share answer or an AI suggestion.
+ */
+ id?: string;
+ /**
+ * Assigned by the store; every change to a stored layout moves it by one.
+ */
+ revision?: number;
+ /**
+ * Set by OpenVibe.Network: the one layout a product draws for this surface on this device class. A draft, a share answer and an AI suggestion carry no active field.
+ */
+ active?: boolean;
+ surface: string;
+ surface_version: number;
+ device: "phone" | "tablet" | "computer";
+ /**
+ * The person's name for this layout; "My phone controls" is fine.
+ */
+ name: string;
+ /**
+ * The author's display name and, for a private or link layout, the subject. A share or gallery answer carries the display name only, never the account.
+ */
+ author: {
+ name: string;
+ subject?: SubjectRef;
+ };
+ sharing: "private" | "link" | "gallery";
+ /**
+ * @minItems 1
+ * @maxItems 64
+ */
+ parts: [
+ {
+ id: string;
+ visible: boolean;
+ place:
+ | {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+ }
+ | {
+ dock: "left" | "right" | "top" | "bottom";
+ order: number;
+ w: number;
+ h: number;
+ };
+ style: {
+ /**
+ * A theme token, never a colour: the theme decides what it is, so contrast follows the theme.
+ */
+ tint: string;
+ shape: "round" | "pill" | "square";
+ opacity: number;
+ label: boolean;
+ };
+ bindings: {
+ /**
+ * @maxItems 32
+ */
+ keys: string[];
+ /**
+ * @maxItems 32
+ */
+ gamepad: string[];
+ };
+ },
+ ...{
+ id: string;
+ visible: boolean;
+ place:
+ | {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+ }
+ | {
+ dock: "left" | "right" | "top" | "bottom";
+ order: number;
+ w: number;
+ h: number;
+ };
+ style: {
+ /**
+ * A theme token, never a colour: the theme decides what it is, so contrast follows the theme.
+ */
+ tint: string;
+ shape: "round" | "pill" | "square";
+ opacity: number;
+ label: boolean;
+ };
+ bindings: {
+ /**
+ * @maxItems 32
+ */
+ keys: string[];
+ /**
+ * @maxItems 32
+ */
+ gamepad: string[];
+ };
+ }[]
+ ];
+ updated_at?: string;
+}
+
+/** [email protected] (owner: contracts) */
+/**
+ * ui.layout-request@1: what an editor sends OpenVibe.AI's ui.layout.suggest task, and what Network forwards for a signed-in person. It is the surface's catalog, the layout shown now (or null for the default one) and the words the person typed (max 500 characters), for one device class. The catalog is the only power the answer can use; the current layout is the starting arrangement; the words never contain markup that becomes part of it.
+ */
+export interface UiLayoutRequest {
+ catalog: UiSurface;
+ /**
+ * The arrangement shown now, or null when the person is looking at the default.
+ */
+ layout: UiLayout | null;
+ /**
+ * The person's own description, e.g. "put Stop on the left and make it huge".
+ */
+ words: string;
+ device: "phone" | "tablet" | "computer";
+}
+
+/** [email protected] (owner: contracts) */
+/**
+ * ui.layout-suggestion@1: OpenVibe.AI's answer to ui.layout.suggest. ok true carries the layout, already checked by openvibe-sdk/ui-layout against the catalog sent with the request, and a one-sentence summary of what changed plus the model's reasons. ok false is an honest refusal: no layout, one sentence saying what it could not do, and the validator's reasons in plain words (for example "Stop has to stay visible."). A receipt names the model that answered, with the run and the route. The suggestion is previewed, never applied silently; Network or the product stores it only after the person accepts it.
+ */
+export type UiLayoutSuggestion = {
+ [k: string]: unknown | undefined;
+} & {
+ ok: boolean;
+ /**
+ * The validated layout when ok is true; null when it is false.
+ */
+ layout: UiLayout | null;
+ /**
+ * One sentence saying what changed, or what could not be done.
+ */
+ summary: string;
+ /**
+ * Why this arrangement, or the validator's reasons for refusing one.
+ *
+ * @maxItems 20
+ */
+ reasons:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ /**
+ * Which model answered: the same receipt every AI run carries, at the task's own shape.
+ */
+ receipt?: {
+ model: string;
+ provider?: string;
+ route?: string;
+ run_id?: string;
+ cost_usd?: number;
+ };
+};
+
+/** [email protected] (owner: network) */
+/**
+ * network.layout.changed v1 (OpenVibe.Network server/layouts, written into network_event_outbox in the transaction that changes a stored layout). One event per change to one person's ui.layout@1: set active, saved, renamed, deleted, shared, unshared, published to the gallery, unpublished, copied from the gallery, or reset to the default. Envelope: subject { type: user, id } and the layout's revision, actor the person, visibility internal. The payload carries no account data and no layout contents, so an open page can announce a change and refetch with the person's own token.
+ */
+export interface NetworkLayoutChangedPayload {
+ surface: string;
+ device: "phone" | "tablet" | "computer";
+ layout_id: string;
+ change:
+ | "active"
+ | "saved"
+ | "renamed"
+ | "deleted"
+ | "shared"
+ | "unshared"
+ | "published"
+ | "unpublished"
+ | "copied"
+ | "reset";
+ revision: number;
+ sharing?: "private" | "link" | "gallery";
+}
+
+/** [email protected] (owner: contracts) */
+/**
+ * ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop, the e-stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.
+ */
+export type UiAction =
+ | {
+ kind: "robot.drive_pulse";
+ product: "bot";
+ direction: "forward" | "back" | "left" | "right" | "spin_left" | "spin_right";
+ ms: number;
+ /**
+ * Fraction of the robot's allowed speed; omitted means the owner's default for the role.
+ */
+ speed?: number;
+ }
+ | {
+ kind: "robot.head_preset";
+ product: "bot";
+ /**
+ * A head tilt preset. Bot still checks the robot profile and the owner's limits.
+ */
+ preset: "up" | "center" | "down";
+ }
+ | {
+ kind: "robot.headlight";
+ product: "bot";
+ on: boolean;
+ }
+ | {
+ kind: "robot.say";
+ product: "bot";
+ text: string;
+ }
+ | {
+ kind: "robot.macro";
+ product: "bot";
+ macro: string;
+ }
+ | {
+ kind: "chat.message";
+ product: "chat";
+ text: string;
+ }
+ | {
+ kind: "chat.pin";
+ product: "chat";
+ /**
+ * trigger pins the message that caused this action; latest pins the newest chat message.
+ */
+ source: "trigger" | "latest";
+ }
+ | {
+ kind: "chat.poll";
+ product: "chat";
+ question: string;
+ /**
+ * @minItems 2
+ * @maxItems 10
+ */
+ options:
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string];
+ }
+ | {
+ kind: "screen.alert";
+ product: "live";
+ title: string;
+ text?: string;
+ }
+ | {
+ kind: "screen.animation";
+ product: "live";
+ /**
+ * An animation the page or overlay has; an unknown name is refused, never fetched.
+ */
+ name: string;
+ }
+ | {
+ kind: "screen.confetti";
+ product: "live";
+ intensity?: "low" | "medium" | "high";
+ }
+ | {
+ kind: "screen.sound";
+ product: "live";
+ /**
+ * A sound from the streamer's own sound list; Live never fetches a URL for it.
+ */
+ sound: string;
+ volume?: number;
+ }
+ | {
+ kind: "screen.tts";
+ product: "live";
+ text: string;
+ /**
+ * A voice from the streamer's own voice list.
+ */
+ voice: string;
+ }
+ | {
+ kind: "screen.counter";
+ product: "live";
+ key: string;
+ by: number;
+ }
+ | {
+ kind: "screen.state";
+ product: "live";
+ key: string;
+ value: boolean | number | string | (number | string)[];
+ }
+ | {
+ kind: "screen.visibility";
+ product: "live";
+ element: string;
+ visible: boolean;
+ }
+ | {
+ kind: "screen.scene";
+ product: "live";
+ /**
+ * A scene the layout has; an unknown scene is refused.
+ */
+ scene: string;
+ }
+ | {
+ kind: "stream.clip";
+ product: "live";
+ /**
+ * How far back the clip starts, at most 30 seconds.
+ */
+ seconds?: number;
+ }
+ | {
+ kind: "stream.moment";
+ product: "live";
+ label?: string;
+ }
+ | {
+ kind: "stream.title";
+ product: "live";
+ title: string;
+ }
+ | {
+ kind: "stream.category";
+ product: "live";
+ category: string;
+ }
+ | {
+ kind: "ai.ask";
+ product: "ai";
+ prompt: string;
+ model?: string;
+ max_cost_usd?: number;
+ }
+ | {
+ kind: "actor.task";
+ product: "actor";
+ task: string;
+ }
+ | {
+ kind: "webhook.call";
+ product: "live";
+ endpoint: string;
+ }
+ | {
+ kind: "money.tip_goal";
+ product: "billing";
+ goal: string;
+ };
+
+/** [email protected] (owner: contracts) */
+/**
+ * ui.element@1: an Element people make and place on a surface: a look, a behaviour and, only when a recipe is not enough, a mod. LOOK draws from OpenVibe.Design's components and the theme's tokens, so it follows the person's theme, the seasons and dark/light; it carries no markup and no URL. RECIPE is data only: one or more triggers from ui.trigger@1, the conditions that decide who may fire it and what it costs, and one or more actions from ui.action@1, checked against CAPABILITIES. A capability is an action kind the element declares; the host refuses an action the element did not declare, and the product named by the action still checks the presser's role, the cooldown, the cost and its own limits. MOD is an optional reference to a code module (id, version and digest) for the stage where a recipe is not enough; the module never adds a power. Conditions use channel points only in stage 1: no Vibes, no money, and Billing still confirms any charge itself. The id and revision are assigned by the store; a draft has neither.
+ */
+export interface UiElement {
+ /**
+ * Assigned by the store when the element is saved; absent in a draft.
+ */
+ id?: string;
+ /**
+ * Assigned by the store; every saved change moves it by one.
+ */
+ revision?: number;
+ name: string;
+ description?: string;
+ /**
+ * The author's display name and, for a private or link element, the subject. A gallery or copy answer carries the display name only, never the account.
+ */
+ author: {
+ name: string;
+ subject?: SubjectRef;
+ };
+ sharing: "private" | "link" | "gallery";
+ look: {
+ /**
+ * An OpenVibe.Design component name (design.component@1), e.g. button or meter. A name the page does not have is refused, never fetched.
+ */
+ component: string;
+ /**
+ * The words on the element, e.g. Make it dance.
+ */
+ label: string;
+ /**
+ * A glyph name from the design icon set.
+ */
+ icon?: string;
+ /**
+ * The design component's own props (the sliders the editor shows). Numbers, strings and booleans only; no nesting.
+ */
+ props?: {
+ [k: string]: (boolean | number | string) | undefined;
+ };
+ style?: {
+ /**
+ * A theme token, never a colour: the theme decides what it is, so contrast follows the theme.
+ */
+ tint?: string;
+ shape?: "round" | "pill" | "square";
+ opacity?: number;
+ show_label?: boolean;
+ };
+ };
+ recipe: {
+ /**
+ * @minItems 1
+ * @maxItems 8
+ */
+ triggers:
+ | [UiTrigger]
+ | [UiTrigger, UiTrigger]
+ | [UiTrigger, UiTrigger, UiTrigger]
+ | [UiTrigger, UiTrigger, UiTrigger, UiTrigger]
+ | [UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger]
+ | [UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger]
+ | [UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger]
+ | [UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger, UiTrigger];
+ conditions?: {
+ /**
+ * Who may fire it. A higher role admits the lower ones where the product says so (an operator is admitted to subscriber-only); owner is the channel's owner alone. Default everyone.
+ */
+ role?: "everyone" | "follower" | "subscriber" | "vip" | "operator" | "owner";
+ /**
+ * Channel points the presser spends, from Live's points. Stage 1 has no other currency, and an element never charges money.
+ */
+ cost?: {
+ points: number;
+ };
+ /**
+ * How long the same person must wait, and how long everybody must wait, before it can fire again.
+ */
+ cooldown?: {
+ per_person_seconds?: number;
+ overall_seconds?: number;
+ };
+ /**
+ * The most times it may fire in a UTC day, per person and for everyone.
+ */
+ daily_cap?: {
+ per_person?: number;
+ overall?: number;
+ };
+ /**
+ * When true the element fires only while the channel is live.
+ */
+ live_only?: boolean;
+ /**
+ * When true a robot action is requested only while the presser has the robot's turn; Bot still decides.
+ */
+ robot_turn_only?: boolean;
+ /**
+ * Fire only one time in N. The host draws the lot, so a refused draw costs nothing.
+ */
+ chance?: {
+ one_in: number;
+ };
+ };
+ /**
+ * @minItems 1
+ * @maxItems 16
+ */
+ actions:
+ | [UiAction]
+ | [UiAction, UiAction]
+ | [UiAction, UiAction, UiAction]
+ | [UiAction, UiAction, UiAction, UiAction]
+ | [UiAction, UiAction, UiAction, UiAction, UiAction]
+ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]
+ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]
+ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]
+ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]
+ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]
+ | [UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction, UiAction]
+ | [
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction
+ ]
+ | [
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction
+ ]
+ | [
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction
+ ]
+ | [
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction
+ ]
+ | [
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction,
+ UiAction
+ ];
+ };
+ /**
+ * The action kinds this element may request. Every action in the recipe must be declared here; the host refuses the rest.
+ *
+ * @minItems 1
+ * @maxItems 32
+ */
+ capabilities: [
+ (
+ | "robot.drive_pulse"
+ | "robot.head_preset"
+ | "robot.headlight"
+ | "robot.say"
+ | "robot.macro"
+ | "chat.message"
+ | "chat.pin"
+ | "chat.poll"
+ | "screen.alert"
+ | "screen.animation"
+ | "screen.confetti"
+ | "screen.sound"
+ | "screen.tts"
+ | "screen.counter"
+ | "screen.state"
+ | "screen.visibility"
+ | "screen.scene"
+ | "stream.clip"
+ | "stream.moment"
+ | "stream.title"
+ | "stream.category"
+ | "ai.ask"
+ | "actor.task"
+ | "webhook.call"
+ | "money.tip_goal"
+ ),
+ ...(
+ | "robot.drive_pulse"
+ | "robot.head_preset"
+ | "robot.headlight"
+ | "robot.say"
+ | "robot.macro"
+ | "chat.message"
+ | "chat.pin"
+ | "chat.poll"
+ | "screen.alert"
+ | "screen.animation"
+ | "screen.confetti"
+ | "screen.sound"
+ | "screen.tts"
+ | "screen.counter"
+ | "screen.state"
+ | "screen.visibility"
+ | "screen.scene"
+ | "stream.clip"
+ | "stream.moment"
+ | "stream.title"
+ | "stream.category"
+ | "ai.ask"
+ | "actor.task"
+ | "webhook.call"
+ | "money.tip_goal"
+ )[]
+ ];
+ /**
+ * The element's state before anything runs; the counter starts at 0, never at a value the element did not declare.
+ */
+ initial_state?: {
+ [k: string]: (boolean | number | string | (number | string)[]) | undefined;
+ };
+ /**
+ * A code module for an element whose recipe is not enough (stage 3). It runs sandboxed; the reference is data, and the host checks every request the module makes against the element's capabilities.
+ */
+ mod?: {
+ id: string;
+ version: string;
+ sha256: string;
+ /**
+ * The subset of the element's capabilities the module itself may request; omitted means the element's declared capabilities, and a product may grant fewer.
+ *
+ * @maxItems 32
+ */
+ capabilities?: (
+ | "robot.drive_pulse"
+ | "robot.head_preset"
+ | "robot.headlight"
+ | "robot.say"
+ | "robot.macro"
+ | "chat.message"
+ | "chat.pin"
+ | "chat.poll"
+ | "screen.alert"
+ | "screen.animation"
+ | "screen.confetti"
+ | "screen.sound"
+ | "screen.tts"
+ | "screen.counter"
+ | "screen.state"
+ | "screen.visibility"
+ | "screen.scene"
+ | "stream.clip"
+ | "stream.moment"
+ | "stream.title"
+ | "stream.category"
+ | "ai.ask"
+ | "actor.task"
+ | "webhook.call"
+ | "money.tip_goal"
+ )[];
+ };
+ created_at?: string;
+ updated_at?: string;
+}
+
+/** [email protected] (owner: contracts) */
+/**
+ * ui.element-state@1: the small store an Element keeps for itself (counters, scores, lists, a wheel's entries), as Live broadcasts it to everyone watching and as a read returns it. CHANNEL and ELEMENT_ID name the element instance, REVISION grows by one with each accepted change so a late frame never overwrites a newer one, and VALUES is the store itself: at most 32 keys, each a boolean, a number, a short string or a list of those. It is state, not code: no objects, no markup, no URL and no account data, which keeps a shared element's state safe to render for every viewer.
+ */
+export interface UiElementState {
+ /**
+ * The Live channel's username (its slug).
+ */
+ channel: string;
+ element_id: string;
+ revision: number;
+ /**
+ * The element's own store. A key is lower-case and 32 characters at most; a value is a boolean, a number, a string of 200 characters at most or a list of those.
+ */
+ values: {
+ [k: string]: (boolean | number | string | (number | string)[]) | undefined;
+ };
+ updated_at: string;
+}
+
+/** [email protected] (owner: contracts) */
+/**
+ * ui.trigger@1: one trigger an Element's recipe listens for, from the fixed catalog. kind says what happened: a viewer pressing, holding or double-pressing the element, a key or gamepad binding, a chat command or text, a follow, a tip, an AI Moment, a Rundown topic, a timer or schedule, robot telemetry or an event, the stream starting or ending, a viewer-count crossing or another element's event. The fields a kind needs are the only fields it accepts. A trigger is data: it never names code or a URL, and a product may refuse a kind it does not offer yet. Stage 1 of Elements uses press and chat_command; the rest are declared here so a stage-2 element and the editor speak one language.
+ */
+export type UiTrigger =
+ | {
+ kind: "press";
+ }
+ | {
+ kind: "hold";
+ ms: number;
+ }
+ | {
+ kind: "double_press";
+ /**
+ * The longest gap between the two presses; 400 by default.
+ */
+ within_ms?: number;
+ }
+ | {
+ kind: "key";
+ key: string;
+ /**
+ * @maxItems 4
+ */
+ modifiers?:
+ | []
+ | ["ctrl" | "alt" | "shift" | "meta"]
+ | ["ctrl" | "alt" | "shift" | "meta", "ctrl" | "alt" | "shift" | "meta"]
+ | ["ctrl" | "alt" | "shift" | "meta", "ctrl" | "alt" | "shift" | "meta", "ctrl" | "alt" | "shift" | "meta"]
+ | [
+ "ctrl" | "alt" | "shift" | "meta",
+ "ctrl" | "alt" | "shift" | "meta",
+ "ctrl" | "alt" | "shift" | "meta",
+ "ctrl" | "alt" | "shift" | "meta"
+ ];
+ }
+ | {
+ kind: "gamepad";
+ button: string;
+ /**
+ * Which connected pad; omitted means any.
+ */
+ index?: number;
+ }
+ | {
+ kind: "chat_command";
+ /**
+ * The command without arguments, lower case, e.g. !dance.
+ */
+ command: string;
+ }
+ | {
+ kind: "chat_text";
+ /**
+ * @minItems 1
+ * @maxItems 16
+ */
+ words:
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ /**
+ * any (default) matches one of the words; all matches every one.
+ */
+ mode?: "any" | "all";
+ }
+ | {
+ kind: "follow";
+ }
+ | {
+ kind: "tip";
+ /**
+ * The smallest tip, in Vibes, that fires the trigger; omitted means any tip.
+ */
+ min?: number;
+ }
+ | {
+ kind: "ai_moment";
+ }
+ | {
+ kind: "rundown_topic";
+ /**
+ * Optional words the topic has to mention; omitted means any topic.
+ *
+ * @maxItems 8
+ */
+ words?:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string];
+ }
+ | {
+ kind: "timer";
+ every_seconds: number;
+ /**
+ * live (default) counts only while the channel is live; always counts from the element being active.
+ */
+ start?: "live" | "always";
+ }
+ | {
+ kind: "schedule";
+ /**
+ * A five-field cron in the person's timezone.
+ */
+ cron: string;
+ timezone: string;
+ }
+ | {
+ kind: "robot_telemetry";
+ metric: "battery_percent" | "latency_ms" | "stuck" | "temperature_c";
+ op: "lt" | "gt";
+ value: number;
+ /**
+ * How long the condition must hold; 0 (default) means at once.
+ */
+ for_seconds?: number;
+ }
+ | {
+ kind: "robot_event";
+ event: "estop" | "stopped" | "disconnected" | "command_refused" | "button";
+ }
+ | {
+ kind: "stream_start";
+ }
+ | {
+ kind: "stream_end";
+ }
+ | {
+ kind: "viewer_count";
+ op: "above" | "below";
+ count: number;
+ }
+ | {
+ kind: "element_event";
+ element_id: string;
+ /**
+ * pressed (default) fires after that element ran; state and shown fire when Live broadcasts them.
+ */
+ event?: "pressed" | "state" | "shown";
+ };
+
+/** [email protected] (owner: live) */
+/**
+ * live.element.pressed v1 (OpenVibe.Live server/elements): a viewer's element press was accepted and ran. It carries the public facts the stream should see: the channel, the element's id and name, the presser's public name and subject, the trigger that fired, the channel points it cost and the element-state revision it left behind. It never carries the presser's email, address, account balance or anything private; a refused press does not produce it. Envelope: subject { type: element, id: el_… }, actor the presser's subject, visibility public, priority low. Also written to the channel's AI timeline as a moment, so the stream's own timeline can cite 'chat made the robot dance'.
+ */
+export interface LiveElementPressedPayload {
+ /**
+ * The public channel facts (the channel page is public already).
+ */
+ channel: {
+ username: string;
+ display_name: string;
+ };
+ element_id: string;
+ element_name: string;
+ presser: SubjectRef;
+ /**
+ * The display name the stream shows, never an email or an account id.
+ */
+ presser_name: string;
+ trigger: UiTrigger;
+ /**
+ * Channel points the press cost; 0 when it was free.
+ */
+ points_spent?: number;
+ /**
+ * The element-state revision after the actions ran; 0 when the element has no state yet.
+ */
+ state_revision?: number;
+ at: string;
+}
+
+/** [email protected] (owner: live) */
+/**
+ * live.element.state v1 (OpenVibe.Live server/elements): an element's own store changed, broadcast to everyone watching through Live's existing events path. Carries the channel's public facts and the same store as ui.element-state@1: the element instance, a revision that grows by one per accepted change, the values themselves and when. It is public state (a counter, a score, a wheel's entries), never the presser's data: a value may not be an object, a markup string or a URL, and the payload carries no account.
+ */
+export interface LiveElementStatePayload {
+ channel: {
+ username: string;
+ display_name: string;
+ };
+ element_id: string;
+ revision: number;
+ /**
+ * The element's own store. A key is lower-case and 32 characters at most; a value is a boolean, a number, a string of 200 characters at most or a list of those.
+ */
+ values: {
+ [k: string]: (boolean | number | string | (number | string)[]) | undefined;
+ };
+ updated_at: string;
+}
+
+/** [email protected] (owner: ai) */
+/**
+ * A deep dive: expert design research run by OpenVibe.AI (workflow design.deep_dive) for OpenVibe.Exchange's /deep-dive page and, through the API, OpenVibe.Design. One document holds the question and depth (the input), the seven steps, every source read, every capture (a private Media object per viewport and theme, with its DOM facts and Expert extras), the per-site analysis, the synthesis across sites, the recommendations checked against OpenVibe's own rules, and the receipt of every model call and capture. Every claim carries its evidence: the site, the capture or crop, the URL and the date it was read. Nothing is copied: captures are private to the run's owner and the report shows them as attributed commentary.
+ */
+export interface AiDeepDive {
+ /**
+ * The run id; a deep dive is a run (GET /api/v1/deep-dives/{id}).
+ */
+ id: string;
+ schema: "ai.deep-dive@1";
+ run_id: string;
+ status: "queued" | "running" | "choosing" | "succeeded" | "failed" | "cancelled";
+ input: {
+ question: string;
+ depth: "quick" | "thorough" | "expert";
+ max_sites: number;
+ /**
+ * Sites the person named when starting the run.
+ *
+ * @maxItems 30
+ */
+ sites?: string[];
+ };
+ steps: {
+ key: "plan" | "search" | "choose" | "capture" | "analyse" | "synthesise" | "report";
+ status: "pending" | "running" | "done" | "skipped" | "failed";
+ detail?: string | null;
+ started_at?: string | null;
+ finished_at?: string | null;
+ }[];
+ sources: {
+ id: string;
+ kind: "web" | "picture" | "icon" | "project" | "rules" | "component" | "named";
+ title: string;
+ url: string;
+ publisher?: string | null;
+ engine?: string | null;
+ snippet?: string | null;
+ license?: string | null;
+ read_at: string;
+ }[];
+ captures: {
+ id: string;
+ source_id: string;
+ url: string;
+ final_url?: string | null;
+ title?: string | null;
+ captured_at: string;
+ viewport: 390 | 1280;
+ theme: "light" | "dark";
+ full_page: boolean;
+ media_id?: string | null;
+ width?: number | null;
+ height?: number | null;
+ crops: {
+ id: string;
+ media_id: string;
+ y: number;
+ height: number;
+ }[];
+ dom?: {} | null;
+ extras?: {} | null;
+ /**
+ * Why the capture did not happen: robots_disallowed, not_public, no_chrome, timeout, …
+ */
+ skip?: string | null;
+ cached?: boolean;
+ }[];
+ analysis: {
+ source_id: string;
+ url: string;
+ summary: string;
+ first_impression?: string;
+ hierarchy?: string;
+ density?: string;
+ type?: string;
+ colour?: string;
+ motion?: string;
+ words?: string;
+ /**
+ * @maxItems 12
+ */
+ strengths:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string];
+ /**
+ * @maxItems 12
+ */
+ weaknesses:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string];
+ accessibility?: string | null;
+ speed?: string | null;
+ evidence: {
+ claim: string;
+ source_id?: string | null;
+ capture_id?: string | null;
+ crop_id?: string | null;
+ url: string;
+ read_at: string;
+ }[];
+ }[];
+ synthesis: {
+ patterns: {
+ name: string;
+ what: string;
+ /**
+ * @maxItems 30
+ */
+ sites: string[];
+ usage_count?: number;
+ evidence: {
+ claim: string;
+ source_id?: string | null;
+ capture_id?: string | null;
+ crop_id?: string | null;
+ url: string;
+ read_at: string;
+ }[];
+ }[];
+ comparison: {
+ /**
+ * @maxItems 12
+ */
+ columns:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string];
+ rows:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string][];
+ };
+ best: {
+ source_id: string;
+ what: string;
+ evidence: {
+ claim: string;
+ source_id?: string | null;
+ capture_id?: string | null;
+ crop_id?: string | null;
+ url: string;
+ read_at: string;
+ }[];
+ };
+ mistakes: {
+ what: string;
+ evidence: {
+ claim: string;
+ source_id?: string | null;
+ capture_id?: string | null;
+ crop_id?: string | null;
+ url: string;
+ read_at: string;
+ }[];
+ }[];
+ gap: string;
+ reference_tokens: {
+ palettes: {
+ source_id: string;
+ /**
+ * @maxItems 16
+ */
+ colours:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ }[];
+ type_scales: {
+ source_id: string;
+ /**
+ * @maxItems 8
+ */
+ families:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string];
+ /**
+ * @maxItems 16
+ */
+ sizes:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ }[];
+ spacing: {
+ source_id: string;
+ /**
+ * @maxItems 16
+ */
+ steps:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ }[];
+ };
+ /**
+ * @maxItems 20
+ */
+ do:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ /**
+ * @maxItems 20
+ */
+ dont:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ /**
+ * @maxItems 20
+ */
+ accessibility:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ /**
+ * @maxItems 20
+ */
+ speed:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ };
+ recommendations: {
+ id: string;
+ what: string;
+ why: string;
+ rule: {
+ id?: string | null;
+ status: "follows" | "would_change" | "none";
+ text: string;
+ };
+ evidence: {
+ claim: string;
+ source_id?: string | null;
+ capture_id?: string | null;
+ crop_id?: string | null;
+ url: string;
+ read_at: string;
+ }[];
+ }[];
+ receipt: {
+ price_usd: number;
+ held_usd: number;
+ cost_usd: number;
+ captures_count: number;
+ synthetic: boolean;
+ models: {
+ step: "plan" | "choose" | "analyse" | "synthesise";
+ provider: string | null;
+ model: string | null;
+ tokens_in: number;
+ tokens_out: number;
+ cost_usd: number;
+ ms: number;
+ }[];
+ };
+ /**
+ * @maxItems 40
+ */
+ gaps: string[];
+ created_at: string;
+ updated_at: string;
+ finished_at?: string | null;
+}
+
+/** [email protected] (owner: ai) */
+/**
+ * ai.deep_dive.updated v1 (OpenVibe.AI server/deep-dive): a deep dive's step changed or it ended. One event per step transition and one at the end, so a page (OpenVibe.Exchange's /deep-dive) can follow a run without polling, and OpenVibe.Design can attach a finished report. The payload carries the run and deep-dive ids, the step, the progress and the status; never the question's answer, a capture or a prompt. Envelope: subject { type: run, id: <run_id> }, visibility internal, priority low (important on failed), actor service:ai.
+ */
+export interface AiDeepDiveUpdatedPayload {
+ deep_dive_id: string;
+ run_id: string;
+ status: "queued" | "running" | "choosing" | "succeeded" | "failed" | "cancelled";
+ step: "plan" | "search" | "choose" | "capture" | "analyse" | "synthesise" | "report" | "done" | null;
+ progress: number;
+ steps: {
+ key: "plan" | "search" | "choose" | "capture" | "analyse" | "synthesise" | "report";
+ status: "pending" | "running" | "done" | "skipped" | "failed";
+ }[];
+ depth: "quick" | "thorough" | "expert";
+ requester: SubjectRef;
+ on_behalf_of: SubjectRef | null;
+ url: string;
+ error: {
+ code: string;
+ detail: string;
+ } | null;
+ occurred_at: string;
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "ai.layout.suggest",
+ "version": "1.0",
+ "owner": "ai",
+ "status": "active",
+ "visibility": "first-party",
+ "description": "Turn a person's words into a ui.layout@1 for one surface, using only the parts its ui.surface@1 catalog has. The answer is checked by openvibe-sdk/ui-layout against the same catalog; an invalid answer gets one retry with the validator's reasons, then the task says plainly that it could not do it. The suggestion is previewed, never applied. Runs on the cheap structured-output route with the per-person limits every AI run uses, and its receipt names the model. POST /api/v1/ui/layout/suggest with a service token; OpenVibe.Network calls it for the signed-in person and names them in X-OV-Subject.",
+ "permissions": [
+ "ai:layout"
+ ],
+ "resourceConstraints": [
+ "owner"
+ ],
+ "quotaClass": "ai-run",
+ "events": [],
+ "implementedBy": [
+ "POST /api/v1/ui/layout/suggest"
+ ],
+ "inputSchema": "ui.layout-request@1",
+ "outputSchema": "ui.layout-suggestion@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "network.layout.read",
+ "version": "1.0",
+ "owner": "network",
+ "status": "active",
+ "visibility": "first-party",
+ "description": "Read the active ui.layout@1 one person made for a product's own surface, with that surface's device class. A first-party service presents a service token for openvibe.network and names the person with X-OV-Subject (which must be the person in the path); it reads one layout and nothing else of their account, and never their saved or gallery layouts. The person's own token uses GET /api/v1/me/layouts/:surface instead, which reads and writes only their own rows.",
+ "permissions": [
+ "network:layout"
+ ],
+ "resourceConstraints": [
+ "owner"
+ ],
+ "quotaClass": "standard",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v1/people/:subject/layouts/:surface"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "ui.layout@1"
+}
@@ -36,6 +36,7 @@
@@ -36,6 +36,7 @@
"ai.memory.write",
"ai.memory.write",
"ai.me.read",
"ai.me.read",
"ai.me.manage",
"ai.me.manage",
+ "ai.layout.suggest",
"ai.config.manage",
"ai.config.manage",
"exchange.search.icons"
"exchange.search.icons"
],
],
@@ -45,7 +46,8 @@
@@ -45,7 +46,8 @@
"ai.run.succeeded",
"ai.run.succeeded",
"ai.run.failed",
"ai.run.failed",
"ai.run.cached",
"ai.run.cached",
- "ai.credential.failed"
+ "ai.credential.failed",
+ "ai.deep_dive.updated"
],
],
"eventsConsumed": [],
"eventsConsumed": [],
"namespacesOwned": [
"namespacesOwned": [
@@ -46,7 +46,12 @@
@@ -46,7 +46,12 @@
],
],
"neighbourhood": "watch",
"neighbourhood": "watch",
"strip": [
"strip": [
- { "label": "The Rundown", "icon": "news", "link": "https://openvibe.live/rundown", "line": "What streamers talked about" }
+ {
+ "label": "The Rundown",
+ "icon": "news",
+ "link": "https://openvibe.live/rundown",
+ "line": "What streamers talked about"
+ }
]
]
},
},
"capabilities": [
"capabilities": [
@@ -69,6 +74,8 @@
@@ -69,6 +74,8 @@
],
],
"eventsProduced": [
"eventsProduced": [
"live.agent_stream.changed",
"live.agent_stream.changed",
+ "live.element.pressed",
+ "live.element.state",
"live.index_document.deleted",
"live.index_document.deleted",
"live.index_document.upserted",
"live.index_document.upserted",
"live.moderation.action",
"live.moderation.action",
@@ -93,7 +93,8 @@
@@ -93,7 +93,8 @@
"network.levels.award",
"network.levels.award",
"network.link.access.request",
"network.link.access.request",
"network.agents.register",
"network.agents.register",
- "network.agents.relay"
+ "network.agents.relay",
+ "network.layout.read"
],
],
"eventsProduced": [
"eventsProduced": [
"network.account.deleted",
"network.account.deleted",
@@ -120,6 +121,7 @@
@@ -120,6 +121,7 @@
"network.machine.paired",
"network.machine.paired",
"network.machine.roles_changed",
"network.machine.roles_changed",
"network.machine.revoked",
"network.machine.revoked",
+ "network.layout.changed",
"share.granted",
"share.granted",
"share.revoked",
"share.revoked",
"share.transfer.requested",
"share.transfer.requested",
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.409.0",
+ "version": "0.410.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.409.0",
+ "version": "0.410.0",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.409.0",
+ "version": "0.410.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
"types": "generated/typescript/index.d.ts",
new file mode 100644
new file mode 100644
@@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
+'use strict';
+// Elements (owner ask 2026-11-11): the four contracts and two events that make a button people
+// make. JSON Schema holds most of it; these are the rules it cannot state, and the drift between
+// the one action catalog and the capabilities an element declares.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const contracts = require('..');
+
+const ROOT = path.join(__dirname, '..');
+let n = 0;
+const ok = (cond, msg) => { assert.ok(cond, msg); n++; };
+const fixture = (id, kind, file) => JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures', id, kind, file), 'utf8'));
+
+// ── The action catalog is one list ───────────────────────────────────────
+const actions = contracts.schema('ui.action@1');
+const variants = actions.oneOf;
+const kinds = variants.map((v) => v.properties.kind.const);
+ok(new Set(kinds).size === kinds.length, 'every action kind appears once');
+const capability = actions.$defs.capability.enum;
+ok(JSON.stringify([...capability].sort()) === JSON.stringify([...kinds].sort()),
+ 'ui.action.$defs.capability is exactly the action catalog (an element cannot declare a kind no action has)');
+// The product that authorises each kind is fixed by the catalog, not chosen by the element.
+const PRODUCT = {
+ 'robot.drive_pulse': 'bot', 'robot.head_preset': 'bot', 'robot.headlight': 'bot', 'robot.say': 'bot', 'robot.macro': 'bot',
+ 'chat.message': 'chat', 'chat.pin': 'chat', 'chat.poll': 'chat',
+ 'screen.alert': 'live', 'screen.animation': 'live', 'screen.confetti': 'live', 'screen.sound': 'live',
+ 'screen.tts': 'live', 'screen.counter': 'live', 'screen.state': 'live', 'screen.visibility': 'live',
+ 'screen.scene': 'live', 'stream.clip': 'live', 'stream.moment': 'live', 'stream.title': 'live',
+ 'stream.category': 'live', 'webhook.call': 'live',
+ 'ai.ask': 'ai', 'actor.task': 'actor', 'money.tip_goal': 'billing',
+};
+ok(Object.keys(PRODUCT).length === kinds.length, 'the product map covers every action kind');
+for (const v of variants) {
+ const kind = v.properties.kind.const;
+ ok(v.properties.product.const === PRODUCT[kind], `${kind} is authorised by ${PRODUCT[kind]}`);
+}
+
+// ── The trigger catalog is one list ──────────────────────────────────────
+const triggers = contracts.schema('ui.trigger@1').oneOf;
+const triggerKinds = triggers.map((v) => v.properties.kind.const);
+ok(new Set(triggerKinds).size === triggerKinds.length, 'every trigger kind appears once');
+ok(triggerKinds.includes('press') && triggerKinds.includes('chat_command'),
+ 'stage 1\'s triggers (press, chat_command) are in the catalog');
+
+// ── An element's recipe may only use what it declared ────────────────────
+// JSON Schema can compare a capability with an action only inside one document; this is the
+// cross-field rule openvibe-sdk/elements' validate() enforces too, checked here on the fixtures.
+for (const f of fs.readdirSync(path.join(ROOT, 'fixtures/ui.element/valid')).filter((x) => x.endsWith('.json'))) {
+ const el = fixture('ui.element', 'valid', f);
+ const declared = new Set(el.capabilities);
+ for (const a of el.recipe.actions) ok(declared.has(a.kind), `${f}: ${a.kind} is declared`);
+ if (el.mod && el.mod.capabilities) for (const c of el.mod.capabilities) ok(declared.has(c), `${f}: the mod may request only ${c} if the element declared it`);
+ const text = JSON.stringify(el);
+ ok(!/https?:\/\/|<[a-z!/]/i.test(text), `${f}: an element carries no URL or markup`);
+ const state = el.initial_state || {};
+ for (const [k, v] of Object.entries(state)) {
+ ok(/^[a-z][a-z0-9_]{0,31}$/.test(k), `${f}: state key ${k} is lower-case`);
+ ok(v === null || typeof v !== 'object' || Array.isArray(v), `${f}: state ${k} is a value, not an object`);
+ }
+}
+
+// ── The store is state, not data that can run ────────────────────────────
+const state = contracts.schema('ui.element-state@1');
+ok(!contracts.validate('ui.element-state@1', { channel: 'ada', element_id: 'el_01JAB2C3D4E5F6G7H8J9K0MNPQ', revision: 0, values: { wheel: { a: 1 } }, updated_at: '2026-11-11T20:00:00.000Z' }).valid,
+ 'a state value may not be an object');
+ok(!contracts.validate('ui.element-state@1', { channel: 'ada', element_id: 'el_01JAB2C3D4E5F6G7H8J9K0MNPQ', revision: 0, values: { ok: 'x'.repeat(201) }, updated_at: '2026-11-11T20:00:00.000Z' }).valid,
+ 'a state string is at most 200 characters (the product content-checks the text it shows)');
+ok(state.properties.values.$ref === '#/$defs/values', 'ui.element-state@1 values are the shared values definition');
+
+// ── The events carry the public facts only ───────────────────────────────
+const pressed = contracts.schema('live.element.pressed@1');
+for (const k of ['channel', 'element_id', 'element_name', 'presser', 'presser_name', 'trigger', 'at']) {
+ ok(pressed.required.includes(k), `live.element.pressed carries ${k}`);
+}
+ok(!('email' in pressed.properties) && !('ip' in pressed.properties) && !('balance' in pressed.properties),
+ 'live.element.pressed carries no private account data');
+const changed = contracts.schema('live.element.state@1');
+ok(JSON.stringify(changed.required) === JSON.stringify(['channel', 'element_id', 'revision', 'values', 'updated_at']),
+ 'live.element.state names the store and its revision');
+for (const id of ['live.element.pressed', 'live.element.state']) {
+ const entry = contracts.catalog.find((c) => c.id === id);
+ ok(entry && entry.owner === 'live', `${id} is owned by Live`);
+}
+
+console.log(`elements: ${n} checks passed`);
@@ -42,6 +42,7 @@ sub(path.join(ROOT, 'test/compare-comparison.test.js'));
@@ -42,6 +42,7 @@ sub(path.join(ROOT, 'test/compare-comparison.test.js'));
sub(path.join(ROOT, 'test/link-brand.test.js'));
sub(path.join(ROOT, 'test/link-brand.test.js'));
sub(path.join(ROOT, 'test/link-compute.test.js'));
sub(path.join(ROOT, 'test/link-compute.test.js'));
sub(path.join(ROOT, 'test/money-reasons.test.js'));
sub(path.join(ROOT, 'test/money-reasons.test.js'));
+sub(path.join(ROOT, 'test/elements.test.js'));
// ── Catalog integrity ────────────────────────────────────────────────────
// ── Catalog integrity ────────────────────────────────────────────────────
const files = [];
const files = [];
@@ -141,6 +142,33 @@ ok(contracts.resolve('ai.credential-put@1').schema === 'ai/credential-put.v1.jso
@@ -141,6 +142,33 @@ ok(contracts.resolve('ai.credential-put@1').schema === 'ai/credential-put.v1.jso
&& contracts.resolve('ai.credential-put').version === '2.0.0', 'a contract with two majors resolves by id (newest) or by major');
&& contracts.resolve('ai.credential-put').version === '2.0.0', 'a contract with two majors resolves by id (newest) or by major');
assert.throws(() => contracts.assertValid('identity.subject-ref', { type: 'user', id: '1' }), e => e.code === 'contract.invalid');
assert.throws(() => contracts.assertValid('identity.subject-ref', { type: 'user', id: '1' }), e => e.code === 'contract.invalid');
+// ── UI surfaces and layouts (make your own controls, 2026-11-11) ─────────
+// ui.surface@1 is the catalog a product publishes; ui.layout@1 arranges only what it has. The
+// catalog's own consistency is not something JSON Schema can state: a part's tint has to be one of
+// the surface's theme tokens, part ids are unique, and a never_covers target is a real part.
+{
+ const surface = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/ui.surface/valid/bot-controls.json'), 'utf8'));
+ const partIds = new Set(surface.parts.map((p) => p.id));
+ ok(partIds.size === surface.parts.length, 'ui.surface part ids are unique');
+ for (const p of surface.parts) {
+ for (const tint of p.styles.tints) ok(surface.theme_tokens.includes(tint), `${surface.id}/${p.id} tint ${tint} is a theme token`);
+ for (const target of p.never_covers || []) ok(partIds.has(target), `${surface.id}/${p.id} never_covers names ${target}`);
+ const cols = surface.grid.columns.phone;
+ ok(p.min.w <= cols, `${surface.id}/${p.id} minimum fits the phone grid`);
+ if (p.pressed) ok(p.min.w * surface.grid.cell_px.phone >= 44 && p.min.h * surface.grid.cell_px.phone >= 44, `${surface.id}/${p.id} pressed minimum is at least 44 px`);
+ }
+ // The layout is data only: no URL or markup may appear in a value a layout stores.
+ const layoutText = fs.readFileSync(path.join(ROOT, 'fixtures/ui.layout/valid/phone-controls.json'), 'utf8');
+ ok(!/https?:\/\/|<[a-z]/i.test(layoutText), 'ui.layout carries no URL or markup');
+ // A suggestion says what it could not do rather than carrying an unchecked layout.
+ const suggestion = contracts.validate('ui.layout-suggestion@1', JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/ui.layout-suggestion/valid/suggestion.json'), 'utf8')));
+ ok(suggestion.valid, 'ui.layout-suggestion accepts a validated arrangement with its reasons and receipt');
+ ok(!contracts.validate('ui.layout-suggestion@1', { ok: true, layout: null, summary: 'x', reasons: [] }).valid, 'a successful suggestion must carry a layout');
+ // The event names the change and the layout revision; no account data is in the payload.
+ const changed = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/network.layout.changed/valid/activated.json'), 'utf8'));
+ ok(!('subject' in changed) && !('author' in changed) && !('parts' in changed), 'network.layout.changed carries no account data or layout contents');
+}
+
// ── Manifests ────────────────────────────────────────────────────────────
// ── Manifests ────────────────────────────────────────────────────────────
const serviceIds = new Set(services.manifests.map(m => m.id));
const serviceIds = new Set(services.manifests.map(m => m.id));
for (const m of services.manifests) {
for (const m of services.manifests) {