# SPDX-License-Identifier: LicenseRef-NativeMindNONC
# Event topic tree (02e §7.2) — one tree for WS, socket.io, GraphQL subscriptions, OSC JSON events and GET /events.
# v1 (03e 2.CT.6): every topic of 02e §7.2 + scenarios (RC5), parameters, cluster nodes, visual devices.
# Each entry: `topic` template ({name} = one path level), `kinds` (payload: schemas/api/1.0/event-kinds.schema.json),
# `scope` needed to receive it (02e §3.6; checked per concrete topic, own-principal topics marked), `class`
# (02d §8: S = retained state, latest wins / coalescible; E = event, every message while the client keeps up;
# T = telemetry, droppable), `rate` note.
version: 1
wildcards:
  singleLevel: '+'          # exactly one level, e.g. venues/+/players/main
  multiLevel: '#'           # the rest, last level only, e.g. venues/default/#; '#' alone = everything the principal may read
  authorisation: per concrete topic — denied topics are listed in the subscribe result, existence is never leaked
delivery:
  live: at-most-once per subscription, in seq order
  seq: per-server monotonically increasing `data.seq` (uint, never reused within an instance id)
  ringBuffer: { events: 10000, maxAgeSec: 600 }    # defaults, configurable; GET /events and resume read from it
  resume: subscribe / hello with `sinceSeq` → gap-free replay from the ring buffer, then live
  resync: gap older than the ring buffer, or a slow client's E-class overflow → `resync {topics, reason}`; the client
          re-reads state (REST / GraphQL); retained S-class state is re-sent after resync
  slowConsumer: bounded queue per subscriber (default 256); S coalesced (latest only), T dropped oldest-first,
                E overflow → resync (never silent loss); counters in docs/metrics.md
  positionHz: { default: 4, max: 30 }               # player.position, per subscription option `positionHz`
  analysisHz: { default: 15, max: 60 }              # audio.analysis, per subscription option `analysisHz`
  brokers: [inproc, redis, nats]                    # multi-instance fan-out; NATS maps / → .
topics:
  - topic: system/info
    kinds: [system.info]
    scope: info:read
    class: S
    rate: on change
  - topic: system/health
    kinds: [system.health]
    scope: info:read
    class: S
    rate: on change (≤ 1 Hz)
  - topic: system/logs
    kinds: [log.entry]
    scope: system:read
    class: T
    rate: warn+ by default, level filter in subscribe options; throttled
  - topic: nodes/{nodeId}
    kinds: [node.state]
    scope: info:read
    class: S
    rate: on change; clock offset ≤ 1 Hz (patch)
  - topic: engines/{name}
    kinds: [engine.status, engine.metrics, engine.error]
    scope: info:read
    class: S
    rate: engine.metrics ≤ 1 Hz (patch); others on change
  - topic: venues/{venueId}
    kinds: [venue.updated, venue.deleted, venue.activeShow]
    scope: info:read
    class: S
    rate: on change
  - topic: venues/{venueId}/output
    kinds: [output.state]
    scope: playback:read
    class: S
    rate: on change (latest-value coalescing on slow links)
  - topic: venues/{venueId}/players/{playerId}
    kinds: [player.state, player.position, player.ended, player.error]
    scope: playback:read
    class: S
    rate: player.position default 4 Hz, max 30 Hz (T-class patch); others on change
  - topic: venues/{venueId}/cues
    kinds: [cue.fired, cue.failed]
    scope: playback:read
    class: E
    rate: per cue
  - topic: venues/{venueId}/parameters
    kinds: [parameter.changed]
    scope: playback:read
    class: S
    rate: on change, throttled to 30 Hz per parameter
  - topic: venues/{venueId}/displays/{displayId}
    kinds: [display.state, display.pattern, display.error]
    scope: info:read
    class: S
    rate: on change; display.state temperature ≤ 0.2 Hz
  - topic: venues/{venueId}/calibration
    kinds: [calibration.updated]
    scope: info:read
    class: S
    rate: on change
  - topic: venues/{venueId}/calibration/jobs/{jobId}
    kinds: [calibrationJob.state, calibrationJob.progress, calibrationJob.awaiting]
    scope: info:read
    class: S
    rate: progress ≤ 4 Hz (patch)
  - topic: venues/{venueId}/audio
    kinds: [audio.state]
    scope: playback:read
    class: S
    rate: on change
  - topic: venues/{venueId}/audio/layout
    kinds: [audio.layout]
    scope: info:read
    class: S
    rate: on change
  - topic: venues/{venueId}/audio/levels
    kinds: [audio.levels]
    scope: playback:read
    class: T
    rate: ≤ 10 Hz meters
  - topic: venues/{venueId}/audio/analysis
    kinds: [audioAnalysis.state]
    scope: playback:read
    class: S
    rate: on change
  - topic: venues/{venueId}/audio/analysis/{inputId}
    kinds: [audio.analysis]
    scope: playback:read
    class: T
    rate: default 15 Hz, max 60 Hz per subscription option analysisHz; monitoring only, not kept in the ring buffer (the live path is L2)
  - topic: venues/{venueId}/audio/onsets/{inputId}
    kinds: [audio.onset]
    scope: playback:read
    class: E
    rate: per attack, limited by the input's refractoryMs
  - topic: venues/{venueId}/audio/reactive
    kinds: [audioReactive.state]
    scope: playback:read
    class: S
    rate: on change, including the Resolume connection; stats ≤ 1 Hz (patch)
  - topic: venues/{venueId}/audio/reactive/scenes
    kinds: [audioReactiveScene.updated, audioReactiveScene.deleted]
    scope: content:read
    class: E
    rate: per edit
  - topic: venues/{venueId}/audio/zones/{zoneId}
    kinds: [zone.state, zone.deleted]
    scope: playback:read
    class: S
    rate: on change
  - topic: venues/{venueId}/audio/devices/{deviceId}
    kinds: [audioDevice.state]
    scope: playback:read
    class: S
    rate: on change; telemetry ≤ 1 Hz (patch)
  - topic: venues/{venueId}/audio/sessions/{sessionId}
    kinds: [audioSession.state]
    scope: playback:read
    class: S
    rate: on change; position ≤ 1 Hz (patch)
  - topic: venues/{venueId}/audio/generator
    kinds: [generator.state]
    scope: playback:read
    class: S
    rate: on change
  - topic: venues/{venueId}/texture-sources/{sourceId}
    kinds: [textureSource.state]
    scope: playback:read
    class: S
    rate: on change; fps ≤ 1 Hz (patch)
  - topic: venues/{venueId}/scenarios/{sessionId}
    kinds: [scenarioSession.state, scenarioSession.log]
    scope: playback:read
    class: S
    rate: state on change (S); log lines E-class
  - topic: shows/{showId}
    kinds: [show.updated, show.deleted, playlist.updated, playlist.deleted, timeline.updated, timeline.deleted, cue.updated,
            cue.deleted, trigger.updated, trigger.deleted, matrix.updated]
    scope: content:read
    class: E
    rate: per edit
  - topic: assets/{assetId}
    kinds: [asset.created, asset.progress, asset.ready, asset.failed, asset.deleted]
    scope: content:read
    class: S
    rate: asset.progress ≤ 2 Hz (patch)
  - topic: uploads/{uploadId}
    kinds: [upload.progress]
    scope: content:write
    class: T
    rate: ≤ 2 Hz (patch); visible to the uploader
  - topic: jobs/{jobId}
    kinds: [job.state, job.progress]
    scope: any
    class: S
    rate: progress ≤ 4 Hz (patch); visible to the job owner and system:read
  - topic: gateways/{gatewayId}
    kinds: [gateway.state, gateway.message]
    scope: gateways:manage
    class: S
    rate: state on change; gateway.message is a T-class debug trace, off unless enabled for the gateway
  - topic: apps/{app}
    kinds: [app.payload, app.cleared]
    scope: info:read
    class: E
    rate: per publish
  - topic: clients/{clientId}/commands
    kinds: [client.command]
    scope: any
    class: E
    rate: targeted — only the principal that registered the client may subscribe
  - topic: devices/{deviceId}
    kinds: [device.state]
    scope: info:read
    class: S
    rate: on change; telemetry ≤ 1 Hz (patch)
  - topic: auth/sessions
    kinds: [session.revoked]
    scope: any
    class: E
    rate: own principal only
