// FeedBack capability registry and dispatcher. (function () { 'use strict'; function _fallbackEventTarget() { const listeners = new Map(); return { addEventListener(type, handler, options) { if (typeof handler !== 'function') return; const eventType = String(type || ''); const list = listeners.get(eventType) || []; list.push({ handler, once: !!(options && options.once) }); listeners.set(eventType, list); }, removeEventListener(type, handler) { const eventType = String(type || ''); const list = listeners.get(eventType) || []; listeners.set(eventType, list.filter(entry => entry.handler !== handler)); }, dispatchEvent(event) { if (!event || !event.type) return true; const eventType = String(event.type); const list = (listeners.get(eventType) || []).slice(); for (const entry of list) { entry.handler(event); if (entry.once) this.removeEventListener(eventType, entry.handler); } return true; }, }; } function _ensureFeedBackEventBus() { const existing = window.feedBack && typeof window.feedBack === 'object' ? window.feedBack : null; const hasEventTarget = existing && typeof existing.addEventListener === 'function' && typeof existing.removeEventListener === 'function' && typeof existing.dispatchEvent === 'function'; const bus = hasEventTarget ? existing : (typeof EventTarget === 'function' ? new EventTarget() : _fallbackEventTarget()); if (existing && existing !== bus) { for (const key of Object.keys(existing)) { if (!(key in bus)) bus[key] = existing[key]; } } bus.emit = function (event, detail) { this.dispatchEvent(new CustomEvent(event, { detail })); }; bus.on = function (event, fn, options) { this.addEventListener(event, fn, options); }; bus.off = function (event, fn, options) { this.removeEventListener(event, fn, options); }; window.feedBack = bus; return bus; } _ensureFeedBackEventBus(); if (window.feedBack.capabilities && window.feedBack.capabilities.version === 1) return; const VALID_ROLES = new Set([ 'owner', 'coordinator', 'provider', 'executor', 'observer', 'requester', 'transformer', 'handler', 'validator', 'short-circuiter', 'contributor', ]); const VALID_MODES = new Set(['active', 'optional', 'legacy-shim', 'disabled']); const VALID_COMPATIBILITY = new Set(['none', 'shim-allowed', 'degrade-noop', 'required', 'legacy-window-shim']); const VALID_OWNERSHIP = new Set(['exclusive-owner', 'multi-provider', 'observer-only', 'requester-only', 'privileged', 'diagnostic-only']); const VALID_DOMAIN_KINDS = new Set(['command', 'provider-coordinator', 'event', 'diagnostic', 'privileged']); const VALID_SAFETY = new Set(['safe', 'privileged', 'sensitive', 'diagnostic-only']); const OUTCOMES = new Set([ 'passed', 'transformed', 'handled', 'denied', 'degraded', 'failed', 'short-circuited', 'overridden', 'no-owner', 'no-handler', 'unsupported-command', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'no-target', 'stale', 'cancelled', 'stopped', ]); const MAX_DECISIONS = 100; const MAX_SNAPSHOT_BYTES = 64 * 1024; const DEFAULT_HANDLER_TIMEOUT_MS = 250; // Per-(capability, command) handler-timeout overrides. A few commands front a // user-action / OS-permission prompt (fader reads that await a UI; MIDI/mic // device access) that legitimately runs far longer than the default budget, // so the dispatch/command surface must not fail them at 250 ms while the // operation is still completing through the provider. const COMMAND_TIMEOUTS_MS = { 'audio-mix': { 'get-fader-value': 2100, 'set-fader-value': 2100 }, 'midi-input': { 'discover': 15000, 'open-source': 15000 }, }; function _commandTimeoutFor(capability, commandName) { const byCap = COMMAND_TIMEOUTS_MS[capability]; return byCap ? byCap[commandName] : undefined; } const RESERVED_FUTURE_DOMAINS = new Set([ 'ui.navigation', 'ui.plugin-screens', 'settings', 'backend.routes', 'ui.player-controls', 'ui.player-panels', 'ui.player-overlays', 'plugins', 'jobs', 'midi-control', 'tempo-clock', ]); const RUNTIME_DOMAIN_DEFAULTS = Object.freeze({ library: Object.freeze({ roles: ['provider'], commands: [], operations: ['query-page', 'query-artists', 'query-stats', 'get-art', 'sync-song'], events: [], description: 'Provides library source operations to the core library domain owner.', compatibility: 'none', safety: 'safe', }), }); const CORE_DOMAIN_REVIEW = Object.freeze({ diagnostics: Object.freeze({ lifecycle: 'diagnostic', label: 'Snapshot surface', tone: 'info', summary: 'Read-only diagnostics snapshot/export facade for support bundles and the Capability Inspector.' }), 'audio-input': Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Sensitive input-source identity, availability, and redacted diagnostics for monitoring and later note detection.' }), 'audio-effects': Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Provider-selected effect routes, constrained chain-plan resolution, route/stage controls, fallback, and redaction-safe diagnostics.' }), 'audio-mix': Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated song route, fader, participant, and analyser inspection surface.' }), 'audio-monitoring': Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated monitoring lifecycle, availability, consent, and bridge diagnostics.' }), library: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Current local and plugin-provided library source selection and sync surface.' }), playback: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated song transport, timing, loop, route, requester, bridge, and diagnostics surface.' }), pipeline: Object.freeze({ lifecycle: 'diagnostic', label: 'Graph controls', tone: 'info', summary: 'Capability graph operations: resolve, inspect, validate, and enable or disable participants.' }), stems: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated stem automation, restore, manual override, and compatibility bridge surface backed by the active Stems provider.' }), visualization: Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Core-coordinated highway renderer providers: discovery, picker selection, auto-match attribution, failure fallback, and redaction-safe diagnostics.' }), 'note-detection': Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Coordinates note-detection providers and requester-owned, context-scoped detection bindings (spec 009); consumers own judgment, hit/miss flow as observability events.' }), 'chart-transform': Object.freeze({ lifecycle: 'active', label: 'Active contract', tone: 'clean', summary: 'Coordinates chart-transform providers: pre-render/pre-scoring chart substitution applied after difficulty filtering, with persisted selection, refresh, and fixed-reason failure attribution (#952).' }), }); const EXPECTED_COMPATIBILITY_SHIMS = Object.freeze({}); const pipelines = new Map(); const recentDecisions = []; const missingProviders = []; const compatibilityShims = []; const userOverrides = []; const claimLifecycle = []; const unsupportedVersions = []; const activeClaims = new Map(); const subscribers = new Map(); const knownPlugins = new Map(); let commandSeq = 0; let decisionSeq = 0; function _now() { return new Date().toISOString(); } function _asArray(value) { return Array.isArray(value) ? value : []; } function _uniqueStrings(value, allowed = null) { const out = []; const seen = new Set(); for (const item of _asArray(value)) { if (typeof item !== 'string' || !item.trim()) continue; const normalized = item.trim(); if (allowed && !allowed.has(normalized)) continue; if (seen.has(normalized)) continue; seen.add(normalized); out.push(normalized); } return out; } function _shortDescription(value) { if (typeof value !== 'string') return ''; const normalized = value.replace(/\s+/g, ' ').trim(); return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized; } function _order(order) { const source = order && typeof order === 'object' ? order : {}; return { fixed: !!source.fixed, before: _uniqueStrings(source.before), after: _uniqueStrings(source.after), }; } function _ownershipForKind(kind) { if (kind === 'provider-coordinator') return 'multi-provider'; if (kind === 'diagnostic') return 'diagnostic-only'; if (kind === 'privileged') return 'privileged'; return 'exclusive-owner'; } function _manifestUnsupported(source) { const standards = _standardList(source); if (standards.some(item => /^capability-pipelines\.v(?!1$)/.test(item))) return true; const version = Number(source && source.version); return Number.isFinite(version) && version !== 1; } // Recursive clone of a plain-JSON value into null-prototype objects, so a // consumer mutating a returned copy can't leak back into registry state and // a manifest-controlled `__proto__` key is an ordinary own property rather // than a prototype-pollution vector. function _deepCloneJson(value) { if (Array.isArray(value)) return value.map(_deepCloneJson); if (value && typeof value === 'object') { const copy = Object.create(null); for (const key of Object.keys(value)) copy[key] = _deepCloneJson(value[key]); return copy; } return value; } const VALID_SETTING_TYPES = new Set(['toggle', 'range', 'select']); // Normalize per-instance control descriptors (feedBack#849) declared on a // capability. Lenient (drops malformed entries rather than failing the whole // declaration): the server-side validator in plugins/__init__.py already // strict-checks the /api/plugins manifest path; this keeps runtime // registerParticipant calls forgiving while preserving the descriptors on // the registered participant so generic inspect() — not just the // visualization side channel — sees them. function _normalizeSettings(value) { if (!Array.isArray(value)) return []; const out = []; const seen = new Set(); for (const entry of value) { if (!entry || typeof entry !== 'object') continue; const key = typeof entry.key === 'string' ? entry.key.trim() : ''; if (!key || seen.has(key) || !VALID_SETTING_TYPES.has(entry.type)) continue; const descriptor = { key, type: entry.type }; if (typeof entry.label === 'string' && entry.label.trim()) descriptor.label = entry.label.trim(); // Clone `default` (schema-unconstrained, can be a nested object/array) // so a caller mutating its own object after registerParticipant() // can't reach into registry state. if ('default' in entry) descriptor.default = _deepCloneJson(entry.default); for (const num of ['min', 'max', 'step']) { if (typeof entry[num] === 'number' && Number.isFinite(entry[num])) descriptor[num] = entry[num]; } if (entry.type === 'select' && Array.isArray(entry.options)) { const opts = []; for (const opt of entry.options) { if (!opt || typeof opt !== 'object') continue; const id = typeof opt.id === 'string' ? opt.id.trim() : ''; if (!id) continue; const normalized = { id }; if (typeof opt.label === 'string' && opt.label.trim()) normalized.label = opt.label.trim(); opts.push(normalized); } if (opts.length) descriptor.options = opts; } out.push(descriptor); seen.add(key); } return out; } function _normalizeDeclaration(declaration, container = null) { const source = declaration && typeof declaration === 'object' ? declaration : {}; const mode = VALID_MODES.has(source.mode) ? source.mode : 'active'; const kind = VALID_DOMAIN_KINDS.has(source.kind) ? source.kind : ''; const compatibility = VALID_COMPATIBILITY.has(source.compatibility) ? source.compatibility : 'degrade-noop'; const ownership = VALID_OWNERSHIP.has(source.ownership) ? source.ownership : _ownershipForKind(kind); const safety = VALID_SAFETY.has(source.safety) ? source.safety : 'safe'; const incompatible = !!(source.incompatible || source.unsupportedVersion || _manifestUnsupported(source) || _manifestUnsupported(container)); return { roles: _uniqueStrings(source.roles, VALID_ROLES), events: _uniqueStrings(source.events), commands: _uniqueStrings(source.commands), operations: _uniqueStrings(source.operations || source.providerOperations || source.provider_operations), requests: _uniqueStrings(source.requests), observes: _uniqueStrings(source.observes), emits: _uniqueStrings(source.emits), description: _shortDescription(source.description) || _shortDescription(source.summary), order: _order(source.order), kind, mode: incompatible ? 'disabled' : mode, compatibility, ownership, safety, providerPolicy: source.provider_policy && typeof source.provider_policy === 'object' ? source.provider_policy : {}, settings: _normalizeSettings(source.settings), handlers: source.handlers && typeof source.handlers === 'object' ? source.handlers : {}, eventHandlers: source.eventHandlers && typeof source.eventHandlers === 'object' ? source.eventHandlers : {}, runtime: !!source.runtime, availability: incompatible ? 'incompatible' : (mode === 'disabled' ? 'disabled' : 'available'), incompatible, version: Number(source.version) || 1, }; } function _runtimeDomainRoleList(source) { const roles = _uniqueStrings(source && source.roles, VALID_ROLES); const role = typeof (source && source.role) === 'string' ? source.role.trim() : ''; if (role && VALID_ROLES.has(role) && !roles.includes(role)) roles.push(role); return roles; } function _runtimeDomainDeclaration(domainName, declaration) { if (!domainName || !declaration || typeof declaration !== 'object' || Array.isArray(declaration)) return null; const canonical = _canonicalCapabilityName(domainName); const defaults = RUNTIME_DOMAIN_DEFAULTS[canonical] || {}; const roles = _runtimeDomainRoleList(declaration); const commands = _uniqueStrings(declaration.commands || defaults.commands); const operations = _uniqueStrings(declaration.operations || declaration.providerOperations || declaration.provider_operations || defaults.operations); const requests = _uniqueStrings(declaration.requests || defaults.requests); const observes = _uniqueStrings(declaration.observes || defaults.observes); const emits = _uniqueStrings(declaration.emits || defaults.emits); const events = _uniqueStrings(declaration.events || defaults.events); const kind = VALID_DOMAIN_KINDS.has(declaration.kind) ? declaration.kind : (defaults.kind || ''); const normalized = { roles: roles.length ? roles : _uniqueStrings(defaults.roles, VALID_ROLES), commands, operations, requests, observes, emits, events, kind, mode: VALID_MODES.has(declaration.mode) ? declaration.mode : 'active', compatibility: VALID_COMPATIBILITY.has(declaration.compatibility) ? declaration.compatibility : (defaults.compatibility || 'degrade-noop'), ownership: VALID_OWNERSHIP.has(declaration.ownership) ? declaration.ownership : (defaults.ownership || _ownershipForKind(kind)), safety: VALID_SAFETY.has(declaration.safety) ? declaration.safety : (defaults.safety || 'safe'), }; const description = _shortDescription(declaration.description) || _shortDescription(declaration.summary) || _shortDescription(defaults.description); if (description) normalized.description = description; if (declaration.order && typeof declaration.order === 'object') normalized.order = declaration.order; if (declaration.provider_policy && typeof declaration.provider_policy === 'object') normalized.provider_policy = declaration.provider_policy; if (!normalized.roles.length || (!normalized.commands.length && !normalized.operations.length && !normalized.requests.length && !normalized.events.length && !normalized.observes.length && !normalized.emits.length)) return null; return normalized; } function _runtimeDomainCapabilityMap(declaration) { if (!declaration || typeof declaration !== 'object') return {}; const domains = { ...(declaration.runtime_domains && typeof declaration.runtime_domains === 'object' ? declaration.runtime_domains : {}), ...(declaration.domains && typeof declaration.domains === 'object' ? declaration.domains : {}), }; const capabilities = {}; for (const [domainName, domainDeclaration] of Object.entries(domains)) { const normalized = _runtimeDomainDeclaration(domainName, domainDeclaration); if (normalized) capabilities[_canonicalCapabilityName(domainName)] = normalized; } return capabilities; } function _capabilityMap(declaration) { if (!declaration || typeof declaration !== 'object') return {}; const runtimeCapabilities = _runtimeDomainCapabilityMap(declaration); if (declaration.capabilities && typeof declaration.capabilities === 'object') { return { ...runtimeCapabilities, ...declaration.capabilities }; } if (Object.keys(runtimeCapabilities).length) return runtimeCapabilities; if ('id' in declaration || 'name' in declaration || 'pluginId' in declaration || 'ui_contributions' in declaration || 'ui' in declaration) return {}; return declaration; } function _standardList(source) { if (!source || typeof source !== 'object' || !Array.isArray(source.standards)) return []; const result = []; const seen = new Set(); for (const entry of source.standards) { if (typeof entry !== 'string' || !entry.trim()) continue; const standard = entry.trim(); if (seen.has(standard)) continue; seen.add(standard); result.push(standard); } return result; } function _canonicalCapabilityName(name) { const value = String(name || '').trim(); return value === 'library.providers' ? 'library' : value; } function _rememberPluginManifest(pluginId, declaration) { if (!pluginId || typeof pluginId !== 'string') return; const existing = knownPlugins.get(pluginId) || { pluginId, capabilities: new Set(), standards: new Set(), firstSeenAt: _now(), updatedAt: null, }; for (const standard of _standardList(declaration)) { existing.standards.add(standard); } const caps = _capabilityMap(declaration && declaration.declaration ? declaration.declaration : declaration); for (const [capabilityName, rawDeclaration] of Object.entries(caps)) { const canonicalCapabilityName = _canonicalCapabilityName(capabilityName); if (!canonicalCapabilityName || !rawDeclaration || typeof rawDeclaration !== 'object') continue; if (RESERVED_FUTURE_DOMAINS.has(canonicalCapabilityName)) continue; existing.capabilities.add(canonicalCapabilityName); } if (_manifestUnsupported(declaration)) { _remember(unsupportedVersions, { pluginId, standards: _standardList(declaration), reason: 'Unsupported capability-pipelines version', }); } existing.updatedAt = _now(); knownPlugins.set(pluginId, existing); } function _pluginEntryId(entry) { if (!entry || typeof entry !== 'object') return ''; return typeof entry.pluginId === 'string' ? entry.pluginId : (typeof entry.id === 'string' ? entry.id : ''); } function _pluginEntryDeclaration(entry) { if (!entry || typeof entry !== 'object') return {}; if (entry.declaration && typeof entry.declaration === 'object') return entry.declaration; return _capabilityMap(entry); } function _pipeline(name) { const capabilityName = _canonicalCapabilityName(name); if (!pipelines.has(capabilityName)) { pipelines.set(capabilityName, { name: capabilityName, participants: new Map(), order: [], conflicts: [], resolvedAt: null, }); } return pipelines.get(capabilityName); } function _mergeParticipant(existing, incoming) { if (!existing) return incoming; const runtimeOverride = existing.runtimeOverride; const mergedDeclarationMode = incoming.declarationMode || existing.declarationMode; const mergedMode = runtimeOverride ? (runtimeOverride.enabled ? (mergedDeclarationMode !== 'disabled' ? mergedDeclarationMode : 'active') : 'disabled') : (incoming.mode || existing.mode); return { ...existing, roles: _uniqueStrings([...(existing.roles || []), ...(incoming.roles || [])], VALID_ROLES), events: _uniqueStrings([...(existing.events || []), ...(incoming.events || [])]), commands: _uniqueStrings([...(existing.commands || []), ...(incoming.commands || [])]), operations: _uniqueStrings([...(existing.operations || []), ...(incoming.operations || [])]), requests: _uniqueStrings([...(existing.requests || []), ...(incoming.requests || [])]), observes: _uniqueStrings([...(existing.observes || []), ...(incoming.observes || [])]), emits: _uniqueStrings([...(existing.emits || []), ...(incoming.emits || [])]), order: { fixed: !!(existing.order && existing.order.fixed) || !!(incoming.order && incoming.order.fixed), before: _uniqueStrings([...(existing.order?.before || []), ...(incoming.order?.before || [])]), after: _uniqueStrings([...(existing.order?.after || []), ...(incoming.order?.after || [])]), }, mode: mergedMode, kind: incoming.kind || existing.kind || '', compatibility: incoming.compatibility || existing.compatibility, ownership: incoming.ownership || existing.ownership || 'exclusive-owner', safety: incoming.safety || existing.safety || 'safe', description: incoming.description || existing.description || '', providerPolicy: { ...(existing.providerPolicy || {}), ...(incoming.providerPolicy || {}) }, // Prefer an incoming settings list (a deliberate re-declaration) but // fall back to the existing one — a runtime owner re-registering a // manifest participant (e.g. the viz host adding provider_policy) // doesn't carry settings and must not wipe the manifest's. settings: (incoming.settings && incoming.settings.length) ? incoming.settings : (existing.settings || []), handlers: { ...(existing.handlers || {}), ...(incoming.handlers || {}) }, eventHandlers: { ...(existing.eventHandlers || {}), ...(incoming.eventHandlers || {}) }, runtime: !!existing.runtime || !!incoming.runtime, availability: incoming.availability || existing.availability || 'available', incompatible: !!existing.incompatible || !!incoming.incompatible, version: incoming.version || existing.version || 1, declarationMode: mergedDeclarationMode, runtimeOverride, }; } function _missingOrderPeerConflict(capabilityName, participant, peer, relation) { const inactivePeer = _pipeline(capabilityName).participants.get(peer); if (inactivePeer && inactivePeer.mode === 'disabled') { return { type: 'disabled-order-peer', participant: participant.pluginId, peer, reason: `${relation} peer is registered but disabled`, }; } const knownPeer = knownPlugins.get(peer); if (knownPeer && !(knownPeer.capabilities || new Set()).has(capabilityName)) { return { type: 'missing-capability-peer', participant: participant.pluginId, peer, reason: `${relation} peer is registered but does not declare capability ${capabilityName}`, }; } return { type: 'missing-order-peer', participant: participant.pluginId, peer, reason: `${relation} peer is not registered`, }; } function _participantPriority(participant) { if (participant.roles.includes('owner')) return 0; if (participant.roles.includes('coordinator')) return 1; if (participant.roles.includes('provider')) return 2; if (participant.roles.includes('validator')) return 3; if (participant.roles.includes('transformer')) return 4; if (participant.roles.includes('handler')) return 5; if (participant.roles.includes('short-circuiter')) return 6; return 7; } function _resolvePipeline(name) { const pipeline = _pipeline(name); const participants = Array.from(pipeline.participants.values()) .filter(p => p.mode !== 'disabled') .sort((a, b) => { const fixedDelta = Number(!!b.order.fixed) - Number(!!a.order.fixed); if (fixedDelta) return fixedDelta; const priorityDelta = _participantPriority(a) - _participantPriority(b); if (priorityDelta) return priorityDelta; return a.pluginId.localeCompare(b.pluginId); }); const byId = new Map(participants.map(p => [p.pluginId, p])); const ids = participants.map(p => p.pluginId); const edges = new Map(ids.map(id => [id, new Set()])); const conflicts = []; for (const participant of participants) { for (const before of participant.order.before || []) { if (!byId.has(before)) { conflicts.push(_missingOrderPeerConflict(name, participant, before, 'before')); continue; } edges.get(participant.pluginId).add(before); } for (const after of participant.order.after || []) { if (!byId.has(after)) { conflicts.push(_missingOrderPeerConflict(name, participant, after, 'after')); continue; } edges.get(after).add(participant.pluginId); } } const owners = participants.filter(p => p.roles.includes('owner') && p.ownership !== 'multi-provider'); if (owners.length > 1) { conflicts.push({ type: 'duplicate-owner', participants: owners.map(p => p.pluginId), reason: `Capability ${name} has multiple owners`, }); } const indegree = new Map(ids.map(id => [id, 0])); for (const [, next] of edges) { for (const id of next) indegree.set(id, (indegree.get(id) || 0) + 1); } const baseOrder = new Map(ids.map((id, idx) => [id, idx])); const queue = ids.filter(id => indegree.get(id) === 0) .sort((a, b) => baseOrder.get(a) - baseOrder.get(b)); const insertByBaseOrder = (id) => { const rank = baseOrder.get(id); let low = 0; let high = queue.length; while (low < high) { const mid = (low + high) >>> 1; if (baseOrder.get(queue[mid]) < rank) low = mid + 1; else high = mid; } queue.splice(low, 0, id); }; const resolved = []; while (queue.length) { const id = queue.shift(); resolved.push(id); for (const next of edges.get(id) || []) { indegree.set(next, indegree.get(next) - 1); if (indegree.get(next) === 0) { insertByBaseOrder(next); } } } if (resolved.length !== ids.length) { conflicts.push({ type: 'order-cycle', reason: `Capability ${name} has incompatible ordering constraints` }); pipeline.order = ids; } else { pipeline.order = resolved; } pipeline.conflicts = conflicts; pipeline.resolvedAt = _now(); _emitEvent('pipeline', 'resolved', { capability: name, order: pipeline.order, conflicts }); return pipeline; } function _participantSummary(participant) { return { pluginId: participant.pluginId, capability: participant.capability, roles: participant.roles.slice(), events: participant.events.slice(), commands: participant.commands.slice(), operations: (participant.operations || []).slice(), requests: (participant.requests || []).slice(), observes: (participant.observes || []).slice(), emits: (participant.emits || []).slice(), description: participant.description || '', order: { fixed: !!participant.order.fixed, before: participant.order.before.slice(), after: participant.order.after.slice(), }, mode: participant.mode, declarationMode: participant.declarationMode || participant.mode, enabled: participant.mode !== 'disabled', kind: participant.kind || '', compatibility: participant.compatibility, ownership: participant.ownership || 'exclusive-owner', safety: participant.safety || 'safe', providerPolicy: participant.providerPolicy || {}, // Deep-clone (not shallow): `default` is schema-unconstrained, so a // shallow copy would leak nested-value references and let a consumer // mutate registry state through inspect()/diagnostics. settings: _deepCloneJson(participant.settings || []), availability: participant.availability || (participant.mode === 'disabled' ? 'disabled' : 'available'), incompatible: !!participant.incompatible, version: participant.version || 1, runtime: !!participant.runtime, runtimeOverride: participant.runtimeOverride || null, }; } function _pipelineParticipants(pipeline) { const ordered = []; const seen = new Set(); for (const id of pipeline.order) { const participant = pipeline.participants.get(id); if (!participant) continue; ordered.push(participant); seen.add(id); } const remaining = Array.from(pipeline.participants.values()) .filter(participant => !seen.has(participant.pluginId)) .sort((a, b) => a.pluginId.localeCompare(b.pluginId)); return [...ordered, ...remaining]; } function _pipelineSummary(pipeline) { return { name: pipeline.name, review: _domainReviewSummary(pipeline.name), order: pipeline.order.slice(), resolvedAt: pipeline.resolvedAt, conflicts: pipeline.conflicts.slice(), participants: _pipelineParticipants(pipeline).map(_participantSummary), }; } function _domainReviewSummary(domainName) { return CORE_DOMAIN_REVIEW[domainName] || { lifecycle: 'plugin-defined', label: 'Plugin-defined', tone: 'info', summary: 'Declared by a plugin or test fixture rather than registered as a core FeedBack domain.', }; } function _knownPluginSummary(plugin) { return { pluginId: plugin.pluginId, standards: Array.from(plugin.standards || []).sort(), capabilities: Array.from(plugin.capabilities || []).sort(), firstSeenAt: plugin.firstSeenAt, updatedAt: plugin.updatedAt, }; } function _expectedCompatibilityShimSummary() { return Object.entries(EXPECTED_COMPATIBILITY_SHIMS).flatMap(([capability, shims]) => ( shims.map(shim => ({ capability, status: 'expected', ...shim })) )); } function _redactString(value) { return String(value) .replace(/\/Users\/[^\s/]+(?:\/[^\s]*)?/g, '[path]') .replace(/[A-Za-z]:\\[^\s]+/g, '[path]') .replace(/\b(token|secret|password|api[_-]?key)=([^\s&]+)/gi, '$1=[redacted]'); } function _safeValue(value, depth = 0) { if (typeof value === 'string') return _redactString(value); if (value == null || typeof value === 'number' || typeof value === 'boolean') return value; if (depth > 8) return '[truncated]'; if (Array.isArray(value)) return value.slice(0, 50).map(item => _safeValue(item, depth + 1)); if (typeof value === 'object') { const out = {}; for (const [key, item] of Object.entries(value).slice(0, 50)) { out[_redactString(key)] = _safeValue(item, depth + 1); } return out; } return String(value); } function _recordDecision(decision) { recentDecisions.push({ decisionId: `decision-${++decisionSeq}`, timestamp: _now(), ...decision }); while (recentDecisions.length > MAX_DECISIONS) recentDecisions.shift(); _contributeDiagnostics(); } function _remember(list, entry, max = 50) { list.push({ timestamp: _now(), ...entry }); while (list.length > max) list.shift(); _contributeDiagnostics(); } function _claimKey(capability, claimId) { return `${capability || 'unknown'}:${claimId || 'unknown'}`; } function _targetSelector(source = {}) { const payload = source.payload && typeof source.payload === 'object' ? source.payload : source.args || source; const target = payload.target && typeof payload.target === 'object' ? payload.target : source.target || {}; const value = payload.selector || target.selector || target.id || target.kind || source.selector || '*'; return String(value || '*').toLowerCase(); } function _claimFromContext(ctx) { const payload = ctx.payload && typeof ctx.payload === 'object' ? ctx.payload : {}; const claim = ctx.claim && typeof ctx.claim === 'object' ? ctx.claim : (payload.claim && typeof payload.claim === 'object' ? payload.claim : {}); const claimId = claim.claimId || payload.claimId; if (!claimId) return null; const capability = claim.capability || ctx.capability; return activeClaims.get(_claimKey(capability, claimId)) || { claimId, capability, owner: claim.owner || ctx.requester || 'unknown', createdAt: ctx.timestamp || _now(), synthetic: true, }; } function _claimOwner(capability, source = {}) { const explicitOwner = String(source.owner || '').trim(); if (explicitOwner) return explicitOwner; const pipeline = _resolvePipeline(capability); const owners = (pipeline.order || []) .map(pluginId => pipeline.participants.get(pluginId)) .filter(participant => participant && participant.mode !== 'disabled' && participant.roles.includes('owner')); if (owners.length === 1) return owners[0].pluginId; const exclusiveOwners = owners.filter(participant => participant.ownership !== 'multi-provider'); if (exclusiveOwners.length === 1) return exclusiveOwners[0].pluginId; return 'unknown'; } function _releaseClaimEntry(key, entry, reason = 'Claim released') { activeClaims.delete(key); const released = { ...entry, state: 'released', updatedAt: _now(), restoreSnapshotRef: null, reason, }; _remember(claimLifecycle, released); _emitEvent(entry.capability, 'claim:released', released); return released; } function _orphanClaimEntry(entry, reason) { entry.state = 'orphaned'; entry.updatedAt = _now(); entry.orphanReason = reason; entry.nonDispatchable = true; entry.restoreSnapshotRef = null; _remember(claimLifecycle, { ...entry }); _emitEvent(entry.capability, 'claim:orphaned', entry); } function _cleanupClaimsForParticipant(pluginId, capabilityName = null) { for (const [key, entry] of Array.from(activeClaims.entries())) { if (capabilityName && entry.capability !== capabilityName) continue; if (entry.requester === pluginId) { const released = _releaseClaimEntry(key, entry, `Requester ${pluginId} disappeared`); _recordDecision({ commandId: `release-${released.claimId}`, capability: released.capability, command: 'release', requester: pluginId, participant: 'core', outcome: 'handled', reason: released.reason }); continue; } if (entry.owner === pluginId && entry.state !== 'orphaned') { _orphanClaimEntry(entry, `Owner ${pluginId} disappeared`); _recordDecision({ commandId: `orphan-${entry.claimId}`, capability: entry.capability, command: 'orphan', requester: entry.requester || 'unknown', participant: pluginId, outcome: 'no-owner', reason: entry.orphanReason }); } } } function _overrideMatchesClaim(ctx, claim) { const selector = _targetSelector(ctx); const createdAt = Date.parse(claim.createdAt || '') || 0; return userOverrides.some(entry => { if (entry.type !== 'manual') return false; if (entry.capability !== claim.capability) return false; if ((Date.parse(entry.timestamp || '') || 0) < createdAt) return false; const entrySelector = _targetSelector(entry); return selector === '*' || entrySelector === '*' || selector === entrySelector; }); } function _notifySubscribers(event, detail) { for (const key of [event, '*']) { const handlers = subscribers.get(key) || []; for (const handler of handlers.slice()) { try { handler(detail); } catch (err) { console.warn('[capabilities] subscriber failed:', err); } } } } function _withTimeout(promise, timeoutMs, participant) { // Capture + clear the timer once the race settles — otherwise a handler // that resolves first leaves a live setTimeout (up to timeoutMs) that // keeps the event loop alive and, for long overrides (MIDI permission // commands at 15s), accumulates delayed callbacks across repeated calls. let timer; const timeout = new Promise(resolve => { timer = setTimeout(() => resolve({ outcome: 'failed', reason: `Handler ${participant.pluginId} timed out after ${timeoutMs} ms`, }), timeoutMs); }); return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); } function _normalizeDecision(participant, result) { const source = result && typeof result === 'object' ? result : { outcome: result ? 'handled' : 'passed' }; const outcome = OUTCOMES.has(source.outcome) ? source.outcome : 'handled'; const decision = { participant: participant.pluginId, outcome, status: typeof source.status === 'string' ? source.status : undefined, reason: typeof source.reason === 'string' ? source.reason : undefined, payload: source.payload, }; if (['denied', 'degraded', 'failed', 'short-circuited', 'overridden'].includes(decision.outcome) && !decision.reason) { decision.reason = `${participant.pluginId} returned ${decision.outcome}`; } return decision; } function _finalOutcome(decisions) { if (!decisions.length) return 'degraded'; const terminal = decisions.find(d => ['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'cancelled', 'stopped'].includes(d.outcome)); return terminal ? terminal.outcome : decisions[decisions.length - 1].outcome; } function _blockingConflict(conflicts) { return conflicts.find(conflict => conflict.type === 'duplicate-owner' || conflict.type === 'order-cycle') || null; } function _emitEvent(capability, event, payload) { const capabilityName = _canonicalCapabilityName(capability); if (!capabilityName || RESERVED_FUTURE_DOMAINS.has(capabilityName)) return; const detail = { capability: capabilityName, event, payload: payload || {}, timestamp: _now() }; _notifySubscribers(event, detail); _notifySubscribers(`${capabilityName}:${event}`, detail); try { if (window.feedBack && typeof window.feedBack.emit === 'function') { window.feedBack.emit(`${capabilityName}:${event}`, detail); window.feedBack.emit('capability:event', detail); } else { window.dispatchEvent(new CustomEvent(`${capabilityName}:${event}`, { detail })); window.dispatchEvent(new CustomEvent('feedBack:capability:event', { detail })); } } catch (err) { console.warn('[capabilities] event dispatch failed:', err); } const pipeline = pipelines.get(capabilityName); if (!pipeline) return; for (const participant of pipeline.participants.values()) { const handler = participant.eventHandlers && participant.eventHandlers[event]; if (typeof handler !== 'function') continue; try { handler(detail); } catch (err) { _recordDecision({ commandId: `event-${capability}-${event}`, capability: capabilityName, command: event, requester: detail.payload.source || 'event', participant: participant.pluginId, outcome: 'failed', reason: err && err.message ? err.message : String(err), }); } } } async function command(capabilityName, commandName, context = {}) { capabilityName = _canonicalCapabilityName(capabilityName); const commandId = `command-${++commandSeq}`; if (!capabilityName || RESERVED_FUTURE_DOMAINS.has(capabilityName)) { return { capability: capabilityName, command: commandName, requester: context.requester || 'unknown', outcome: 'degraded', reason: `Capability ${capabilityName || 'unknown'} is reserved for a future domain PR`, decisions: [], }; } const pipeline = _resolvePipeline(capabilityName); const commandContext = { ...context, capability: capabilityName, command: commandName, requester: context.requester || 'unknown', origin: context.origin || 'system', reason: context.reason || 'No reason provided', }; const decisions = []; const blockingConflict = _blockingConflict(pipeline.conflicts || []); if (blockingConflict) { const reason = `Capability ${capabilityName}.${commandName} degraded because ${blockingConflict.reason || blockingConflict.type}`; const decision = { participant: 'core', outcome: 'degraded', reason }; decisions.push(decision); _recordDecision({ commandId, capability: capabilityName, command: commandName, requester: commandContext.requester, origin: commandContext.origin, target: commandContext.target, participant: 'core', outcome: 'degraded', reason, }); return { capability: capabilityName, command: commandName, requester: commandContext.requester, outcome: 'degraded', reason, decisions, }; } const claim = _claimFromContext(commandContext); if (claim && _overrideMatchesClaim(commandContext, claim)) { const reason = `Capability ${capabilityName}.${commandName} skipped because a user override beat claim ${claim.claimId}`; const decision = { participant: 'core', outcome: 'overridden', reason, payload: { claimId: claim.claimId } }; decisions.push(decision); _recordDecision({ commandId, capability: capabilityName, command: commandName, requester: commandContext.requester, origin: commandContext.origin, target: commandContext.target, participant: 'core', outcome: 'overridden', reason, claimId: claim.claimId, }); _emitEvent(capabilityName, 'override', { command: commandName, requester: commandContext.requester, claimId: claim.claimId, target: commandContext.target || commandContext.payload?.target || null }); return { capability: capabilityName, command: commandName, requester: commandContext.requester, outcome: 'overridden', reason, payload: decision.payload, decisions, }; } for (const participantId of pipeline.order) { const participant = pipeline.participants.get(participantId); if (!participant || participant.mode === 'disabled') continue; const handler = participant.handlers && participant.handlers[commandName]; if (typeof handler !== 'function') continue; let decision; try { const timeoutMs = Number(commandContext.timeoutMs || _commandTimeoutFor(capabilityName, commandName) || DEFAULT_HANDLER_TIMEOUT_MS); const result = await _withTimeout(Promise.resolve(handler(commandContext)), timeoutMs, participant); decision = _normalizeDecision(participant, result); } catch (err) { decision = { participant: participant.pluginId, outcome: 'failed', reason: err && err.message ? err.message : String(err), }; } decisions.push(decision); _recordDecision({ commandId, capability: capabilityName, command: commandName, requester: commandContext.requester, origin: commandContext.origin, target: commandContext.target, participant: decision.participant, outcome: decision.outcome, reason: decision.reason, }); if (decision.outcome === 'transformed' && decision.payload && typeof decision.payload === 'object') { commandContext.payload = decision.payload; continue; } if (['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'cancelled', 'stopped'].includes(decision.outcome)) break; } if (!decisions.length) { const reason = `No provider handled ${capabilityName}.${commandName}`; _remember(missingProviders, { capability: capabilityName, command: commandName, requester: commandContext.requester, reason }); const decision = { participant: 'core', outcome: 'no-handler', reason }; decisions.push(decision); _recordDecision({ commandId, capability: capabilityName, command: commandName, requester: commandContext.requester, origin: commandContext.origin, participant: 'core', outcome: 'no-handler', reason, }); } const outcome = _finalOutcome(decisions); const terminalDecision = decisions.find(d => ['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'cancelled', 'stopped'].includes(d.outcome)) || decisions[decisions.length - 1]; return { capability: capabilityName, command: commandName, requester: commandContext.requester, outcome, status: terminalDecision && terminalDecision.status, reason: decisions.find(d => d.reason)?.reason, payload: terminalDecision && terminalDecision.payload, decisions, }; } function _registerParticipant(pluginId, declaration, options = {}) { if (!pluginId || typeof pluginId !== 'string') return; _rememberPluginManifest(pluginId, declaration); const caps = _capabilityMap(declaration); const touched = new Set(); for (const [capabilityName, rawDeclaration] of Object.entries(caps)) { const canonicalCapabilityName = _canonicalCapabilityName(capabilityName); if (!canonicalCapabilityName || !rawDeclaration || typeof rawDeclaration !== 'object') continue; if (RESERVED_FUTURE_DOMAINS.has(canonicalCapabilityName)) continue; const normalized = _normalizeDeclaration(rawDeclaration, declaration); const pipeline = _pipeline(canonicalCapabilityName); const participant = { pluginId, capability: canonicalCapabilityName, ...normalized, declarationMode: normalized.mode, }; if (normalized.incompatible) { participant.handlers = {}; participant.eventHandlers = {}; } pipeline.participants.set(pluginId, _mergeParticipant(pipeline.participants.get(pluginId), participant)); touched.add(canonicalCapabilityName); if (!options.deferResolve) _resolvePipeline(canonicalCapabilityName); _notifySubscribers('registered', { capability: canonicalCapabilityName, pluginId, timestamp: _now() }); } return touched; } function registerParticipant(pluginId, declaration) { _registerParticipant(pluginId, declaration); _contributeDiagnostics(); } function registerOwner(domainName, spec = {}) { const capabilityName = _canonicalCapabilityName(domainName); if (!capabilityName || RESERVED_FUTURE_DOMAINS.has(capabilityName)) return null; const source = spec && typeof spec === 'object' ? spec : {}; const pluginId = typeof source.pluginId === 'string' && source.pluginId ? source.pluginId : `core.${capabilityName}`; const kind = VALID_DOMAIN_KINDS.has(source.kind) ? source.kind : 'command'; const declaration = { roles: ['owner'], commands: _uniqueStrings(source.commands), operations: _uniqueStrings(source.operations || source.providerOperations || source.provider_operations), events: _uniqueStrings(source.events), emits: _uniqueStrings(source.emits), kind, mode: VALID_MODES.has(source.mode) ? source.mode : 'active', compatibility: VALID_COMPATIBILITY.has(source.compatibility) ? source.compatibility : 'none', ownership: VALID_OWNERSHIP.has(source.ownership) ? source.ownership : _ownershipForKind(kind), safety: VALID_SAFETY.has(source.safety) ? source.safety : (kind === 'diagnostic' ? 'diagnostic-only' : 'safe'), description: _shortDescription(source.description) || _shortDescription(source.summary), provider_policy: source.provider_policy && typeof source.provider_policy === 'object' ? source.provider_policy : {}, handlers: source.handlers && typeof source.handlers === 'object' ? source.handlers : {}, eventHandlers: source.eventHandlers && typeof source.eventHandlers === 'object' ? source.eventHandlers : {}, runtime: true, version: 1, }; registerParticipant(pluginId, { [capabilityName]: declaration }); return { pluginId, capability: capabilityName, kind }; } function setParticipantEnabled(pluginId, capabilityName, enabled, options = {}) { capabilityName = _canonicalCapabilityName(capabilityName); if (!pluginId || typeof pluginId !== 'string' || !capabilityName || typeof capabilityName !== 'string') { return { ok: false, reason: 'pluginId and capabilityName are required' }; } if (pluginId === 'core') { return { ok: false, reason: 'Core capability participants cannot be disabled at runtime' }; } const pipeline = pipelines.get(capabilityName); const participant = pipeline && pipeline.participants.get(pluginId); if (!participant) { return { ok: false, reason: `${pluginId} is not registered for ${capabilityName}` }; } const nextEnabled = !!enabled; const previousMode = participant.mode; const restoredMode = participant.declarationMode && participant.declarationMode !== 'disabled' ? participant.declarationMode : 'active'; participant.mode = nextEnabled ? restoredMode : 'disabled'; participant.runtimeOverride = { enabled: nextEnabled, requester: options.requester || 'runtime', reason: options.reason || (nextEnabled ? 'Runtime capability enabled' : 'Runtime capability disabled'), timestamp: _now(), }; const resolved = _resolvePipeline(capabilityName); _emitEvent('pipeline', 'participant.state-changed', { capability: capabilityName, pluginId, enabled: nextEnabled, previousMode, mode: participant.mode, conflicts: resolved.conflicts.slice(), }); _contributeDiagnostics(); return { ok: true, capability: capabilityName, pluginId, enabled: nextEnabled, previousMode, mode: participant.mode, conflicts: resolved.conflicts.slice(), }; } function registerParticipants(entries) { const touched = new Set(); const list = Array.isArray(entries) ? entries : []; for (const entry of list) { const pluginId = _pluginEntryId(entry); if (!pluginId) continue; _rememberPluginManifest(pluginId, entry); } for (const entry of list) { const pluginId = _pluginEntryId(entry); if (!pluginId) continue; const participantCaps = _registerParticipant(pluginId, _pluginEntryDeclaration(entry), { deferResolve: true }); for (const capabilityName of participantCaps || []) touched.add(capabilityName); } for (const capabilityName of Array.from(touched).sort()) _resolvePipeline(capabilityName); _contributeDiagnostics(); return Array.from(touched).sort(); } function unregisterParticipant(pluginId, capabilityName = null) { capabilityName = capabilityName == null ? null : _canonicalCapabilityName(capabilityName); _cleanupClaimsForParticipant(pluginId, capabilityName); for (const [name, pipeline] of pipelines.entries()) { if (capabilityName && name !== capabilityName) continue; if (pipeline.participants.delete(pluginId)) { _resolvePipeline(name); _notifySubscribers('unregistered', { capability: name, pluginId, timestamp: _now() }); } } _contributeDiagnostics(); } function inspect(capabilityName = null) { if (capabilityName) { const pipeline = pipelines.get(_canonicalCapabilityName(capabilityName)); return pipeline ? _pipelineSummary(pipeline) : null; } return Array.from(pipelines.values()).map(_pipelineSummary); } function validateRuntime(options = {}) { const phase = options && typeof options.phase === 'string' ? options.phase : 'runtime'; for (const capabilityName of Array.from(pipelines.keys()).sort()) _resolvePipeline(capabilityName); const snapshot = snapshotDiagnostics(); _emitEvent('pipeline', 'runtime.validated', { phase, conflicts: snapshot.conflicts || [], pipelineCount: (snapshot.pipelines || []).length, participantCount: (snapshot.participants || []).length, }); _contributeDiagnostics(); return snapshot; } function registerCompatibilityShim(shim) { const source = shim && typeof shim === 'object' ? shim : {}; const capability = _canonicalCapabilityName(source.capability || 'unknown') || 'unknown'; if (RESERVED_FUTURE_DOMAINS.has(capability)) return null; const shimId = source.shimId || `${capability}:${source.legacySurface || 'legacy'}`; const existing = compatibilityShims.find(entry => entry.shimId === shimId); const hit = source.status === 'used' || source.used === true || source.hit === true; const explicitHitCount = Number(source.hitCount); const hasExplicitHitCount = Number.isFinite(explicitHitCount) && explicitHitCount >= 0; const previousHitCount = existing && Number.isFinite(Number(existing.hitCount)) ? Number(existing.hitCount) : 0; const hitCount = Math.max(previousHitCount, hasExplicitHitCount ? explicitHitCount : 0) + (hit && !hasExplicitHitCount ? 1 : 0); const status = hit || hitCount > 0 ? 'used' : (source.status || (existing && existing.status) || 'active'); const entry = { ...(existing || {}), shimId, source: source.source || 'unknown', capability, legacySurface: source.legacySurface || 'unknown', status, reason: source.reason, hitCount, lastHitAt: source.lastHitAt || (hit ? _now() : (existing && existing.lastHitAt) || null), }; const providerId = source.providerId || source.provider_id || (existing && existing.providerId); if (providerId) entry.providerId = String(providerId); const ownerPluginId = source.ownerPluginId || source.owner_plugin_id || (existing && existing.ownerPluginId); if (ownerPluginId) entry.ownerPluginId = String(ownerPluginId); if (existing) { const index = compatibilityShims.indexOf(existing); compatibilityShims.splice(index, 1, { timestamp: existing.timestamp || _now(), ...entry }); _contributeDiagnostics(); } else { _remember(compatibilityShims, entry); } return entry; } function _legacyHitSource(source) { if (!source || typeof source !== 'object') return 'legacy-runtime'; const candidate = source.source || source.pluginId || source.owner || source.requester || source.providerId || source.id; if (candidate) return String(candidate); const activePluginId = window.feedBack && (window.feedBack._loadingPluginId || window.feedBack._activePluginId); return activePluginId ? String(activePluginId) : 'legacy-runtime'; } function _legacyHitIdPart(value) { return String(value || 'unknown').replace(/[^a-zA-Z0-9_.:-]+/g, '-'); } function recordLegacyHit(hit) { const source = hit && typeof hit === 'object' ? hit : {}; if (!source.capability || !source.legacySurface) return null; const actor = _legacyHitSource(source); const capability = _canonicalCapabilityName(source.capability); return registerCompatibilityShim({ shimId: source.shimId || `runtime:${_legacyHitIdPart(capability)}:${_legacyHitIdPart(source.legacySurface)}:${_legacyHitIdPart(actor)}`, source: actor, capability, legacySurface: source.legacySurface, status: 'used', reason: source.reason || 'Legacy runtime surface used', hit: true, lastHitAt: source.lastHitAt, providerId: source.providerId || source.provider_id, ownerPluginId: source.ownerPluginId || source.owner_plugin_id, }); } function recordUserOverride(override) { const source = override && typeof override === 'object' ? override : {}; _remember(userOverrides, { type: 'manual', capability: _canonicalCapabilityName(source.capability || 'unknown') || 'unknown', command: source.command, source: source.source || 'unknown', target: source.target, selector: source.selector || _targetSelector(source), reason: source.reason || 'User override recorded', }); } function claim(request = {}) { const source = request && typeof request === 'object' ? request : {}; const capability = _canonicalCapabilityName(source.capability || 'unknown') || 'unknown'; const claimId = source.claimId || source.id; if (!claimId) return () => {}; const key = _claimKey(capability, claimId); if (!activeClaims.has(key)) { const owner = _claimOwner(capability, source); const entry = { claimId, capability, owner, requester: source.requester || source.source || source.owner || 'unknown', targetSelector: _targetSelector(source), state: 'active', createdAt: _now(), updatedAt: _now(), restoreSnapshotRef: source.restoreSnapshotRef || null, reason: source.reason, }; activeClaims.set(key, entry); _recordDecision({ commandId: `claim-${claimId}`, capability, command: 'claim', requester: entry.requester, participant: 'core', outcome: 'handled', reason: entry.reason }); _emitEvent(capability, 'claim:created', entry); } return () => release({ capability, claimId }); } function release(request = {}) { const source = typeof request === 'string' ? { claimId: request } : (request && typeof request === 'object' ? request : {}); if (source.capability) source.capability = _canonicalCapabilityName(source.capability); const claimId = source.claimId || source.id; if (!claimId) return { ok: true, released: false }; let released = null; for (const [key, entry] of Array.from(activeClaims.entries())) { if (entry.claimId !== claimId) continue; if (source.capability && entry.capability !== source.capability) continue; released = _releaseClaimEntry(key, entry, source.reason || 'Claim released'); } if (!released) { _recordDecision({ commandId: `release-${claimId}`, capability: source.capability || 'unknown', command: 'release', requester: source.requester || source.source || source.owner || 'unknown', participant: 'core', outcome: 'degraded', reason: 'Release requested for unknown claim' }); return { ok: true, released: false }; } _recordDecision({ commandId: `release-${claimId}`, capability: released.capability, command: 'release', requester: source.requester || source.source || released.requester || released.owner || source.owner, participant: 'core', outcome: 'handled' }); return { ok: true, released: true, claim: released }; } function _dispatchStatus(result) { if (!result) return 'error'; if (result.status) return result.status; if (result.outcome === 'handled' || result.outcome === 'passed') return 'applied'; if (result.outcome === 'overridden') return 'overridden'; if (result.outcome === 'no-owner') return 'no-owner'; if (result.outcome === 'no-handler') return 'no-handler'; if (result.outcome === 'no-target') return 'no-target'; if (result.outcome === 'unsupported-command') return 'unsupported-command'; if (result.outcome === 'incompatible') return 'incompatible'; if (result.outcome === 'incompatible-version') return 'incompatible-version'; if (result.outcome === 'unavailable') return 'unavailable'; if (result.outcome === 'provider-selection-required') return 'provider-selection-required'; if (result.outcome === 'user-action-required') return 'user-action-required'; if (result.outcome === 'stale') return 'stale'; if (result.outcome === 'cancelled') return 'cancelled'; if (result.outcome === 'stopped') return 'stopped'; if (result.outcome === 'denied' || result.outcome === 'short-circuited') return 'blocked'; if (result.outcome === 'failed') return 'error'; if (result.outcome === 'degraded') return 'no-handler'; return result.outcome || 'error'; } async function dispatch(request = {}) { const source = request && typeof request === 'object' ? request : {}; const capability = _canonicalCapabilityName(source.capability); const commandName = source.command; if (!capability || !commandName) return { status: 'error', outcome: 'failed', reason: 'capability and command are required' }; if (RESERVED_FUTURE_DOMAINS.has(capability)) { return { status: 'no-handler', outcome: 'degraded', capability, command: commandName, reason: `Capability ${capability} is reserved for a future domain PR` }; } const pipeline = _resolvePipeline(capability); const incompatibleParticipants = _pipelineParticipants(pipeline).filter(participant => participant.incompatible); if (incompatibleParticipants.length && incompatibleParticipants.length === pipeline.participants.size) { const reason = `Capability ${capability} is unavailable because all participants are incompatible`; _recordDecision({ commandId: `dispatch-${++commandSeq}`, capability, command: commandName, requester: source.source || source.requester || 'dispatch', participant: 'core', outcome: 'incompatible-version', reason }); return { status: 'incompatible-version', outcome: 'incompatible-version', capability, command: commandName, reason }; } const participants = _pipelineParticipants(pipeline).filter(participant => participant.mode !== 'disabled' && !participant.incompatible); const owners = participants.filter(participant => participant.roles.includes('owner')); const coordinators = participants.filter(participant => participant.roles.includes('coordinator')); if (!owners.length && !coordinators.length) { const reason = `No owner or coordinator registered for ${capability}`; _remember(missingProviders, { capability, command: commandName, requester: source.source || source.requester || 'dispatch', reason }); _recordDecision({ commandId: `dispatch-${++commandSeq}`, capability, command: commandName, requester: source.source || source.requester || 'dispatch', participant: 'core', outcome: 'no-owner', reason }); _emitEvent(capability, 'conflict:missing-provider', { command: commandName, reason }); return { status: 'no-owner', outcome: 'no-owner', capability, command: commandName, reason }; } const commandDeclared = participants.some(participant => (participant.commands || []).includes(commandName)); if (!commandDeclared) { const reason = `Unsupported command ${capability}.${commandName}`; _recordDecision({ commandId: `dispatch-${++commandSeq}`, capability, command: commandName, requester: source.source || source.requester || 'dispatch', participant: 'core', outcome: 'unsupported-command', reason }); return { status: 'unsupported-command', outcome: 'unsupported-command', capability, command: commandName, reason }; } const hasHandler = participants.some(participant => participant.handlers && typeof participant.handlers[commandName] === 'function'); if (!hasHandler) { const reason = `No handler registered for ${capability}.${commandName}`; _remember(missingProviders, { capability, command: commandName, requester: source.source || source.requester || 'dispatch', reason }); _recordDecision({ commandId: `dispatch-${++commandSeq}`, capability, command: commandName, requester: source.source || source.requester || 'dispatch', participant: 'core', outcome: 'no-handler', reason }); return { status: 'no-handler', outcome: 'no-handler', capability, command: commandName, reason }; } const result = await command(capability, commandName, { requester: source.source || source.requester || 'dispatch', origin: source.origin || 'dispatch', reason: source.reason || 'Capability dispatch', target: source.target || source.args?.target || null, payload: source.args || source.payload || {}, claim: source.claim, timeoutMs: source.timeoutMs || _commandTimeoutFor(capability, commandName), }); const status = _dispatchStatus(result); _emitEvent(capability, 'dispatched', { command: commandName, status, result, source: source.source || source.requester || 'dispatch' }); return { ...result, status }; } function subscribe(event, fn) { if (typeof event !== 'string' || typeof fn !== 'function') return () => {}; const handlers = subscribers.get(event) || []; handlers.push(fn); subscribers.set(event, handlers); return () => { const current = subscribers.get(event) || []; subscribers.set(event, current.filter(handler => handler !== fn)); }; } function snapshotDiagnostics() { const pipelineSummaries = inspect(); const snapshot = { schema: 'feedBack.capabilities.diagnostics.v1', pipelines: pipelineSummaries, participants: pipelineSummaries.flatMap(pipeline => Array.isArray(pipeline.participants) ? pipeline.participants : []), recentDecisions: recentDecisions.slice(), conflicts: Array.from(pipelines.values()).flatMap(p => p.conflicts.map(c => ({ capability: p.name, ...c }))), missingProviders: missingProviders.slice(), compatibilityShims: compatibilityShims.slice(), userOverrides: userOverrides.slice(), activeClaims: Array.from(activeClaims.values()), claimLifecycle: claimLifecycle.slice(), unsupportedVersions: unsupportedVersions.slice(), knownPlugins: Array.from(knownPlugins.values()).map(_knownPluginSummary), expectedCompatibilityShims: _expectedCompatibilityShimSummary(), }; let currentSize = JSON.stringify(snapshot).length; while (snapshot.recentDecisions.length && currentSize > MAX_SNAPSHOT_BYTES) { const removedDecision = snapshot.recentDecisions.shift(); currentSize -= JSON.stringify(removedDecision).length; } snapshot.snapshotBytes = JSON.stringify(snapshot).length; return _safeValue(snapshot); } let contributing = false; let capabilitiesChangeScheduled = false; function _scheduleCapabilitiesChanged() { if (capabilitiesChangeScheduled) return; capabilitiesChangeScheduled = true; const schedule = window.queueMicrotask ? window.queueMicrotask.bind(window) : callback => setTimeout(callback, 0); schedule(() => { capabilitiesChangeScheduled = false; const detail = { timestamp: _now() }; try { window.dispatchEvent(new CustomEvent('feedBack:capabilities:changed', { detail })); } catch (_err) { /* capability diagnostics must not break runtime behavior */ } }); } function _contributeDiagnostics() { if (contributing) return; const diagnostics = window.feedBack && window.feedBack.diagnostics; if (diagnostics && typeof diagnostics.contribute === 'function') { contributing = true; try { diagnostics.contribute('capabilities', snapshotDiagnostics()); } catch (_err) { /* diagnostics must never break plugin behavior */ } finally { contributing = false; } } _scheduleCapabilitiesChanged(); } function _coreHandled(payload = {}) { return { outcome: 'handled', payload }; } function _coreDegraded(reason, payload = {}) { return { outcome: 'degraded', reason, payload }; } const api = { version: 1, registerOwner, registerParticipant, registerParticipants, unregisterParticipant, setParticipantEnabled, command, dispatch, claim, release, subscribe, emitEvent: _emitEvent, inspect, validateRuntime, snapshotDiagnostics, getDiagnostics: snapshotDiagnostics, registerCompatibilityShim, recordLegacyHit, recordUserOverride, }; window.feedBack.capabilities = api; registerParticipant('core', { diagnostics: { roles: ['owner', 'provider'], commands: ['snapshot'], events: [], kind: 'diagnostic', description: 'Provides read-only capability snapshots for support bundles and the Capability Inspector.', compatibility: 'none', ownership: 'diagnostic-only', safety: 'diagnostic-only', handlers: { snapshot: () => _coreHandled(snapshotDiagnostics()), }, }, pipeline: { roles: ['owner', 'provider'], commands: ['resolve', 'inspect', 'validate', 'participant.set-enabled'], events: ['resolved', 'runtime.validated', 'participant.state-changed'], kind: 'diagnostic', description: 'Owns capability graph inspection, validation, resolution, and participant enablement commands.', compatibility: 'none', ownership: 'diagnostic-only', safety: 'diagnostic-only', handlers: { resolve: (ctx) => _coreHandled(inspect(ctx.target && ctx.target.capability)), inspect: (ctx) => _coreHandled(inspect(ctx.target && ctx.target.capability)), validate: () => _coreHandled(validateRuntime({ phase: 'pipeline-command' })), 'participant.set-enabled': (ctx) => _coreHandled(setParticipantEnabled( ctx.target && ctx.target.pluginId, ctx.target && ctx.target.capability, !!(ctx.target && ctx.target.enabled), { requester: ctx.requester, reason: ctx.reason } )), }, }, }); try { window.dispatchEvent(new CustomEvent('feedBack:capabilities:ready', { detail: api })); _notifySubscribers('registered', { capability: '*', pluginId: 'core', timestamp: _now() }); } catch (_) {} })();