// src/framework/MatrixActorHtmlElement.ts
//
// MatrixActorHtmlElement — Browser adapter for MatrixActor.
//
// Unified browser element base for actor-backed components.
// - Extends HTMLElement (composition-with-delegation to MatrixActor)
// - Two usage patterns:
//   Pattern A (all-in-one): accepts/emits/handlers on the element class
//   Pattern B (separate actor): getActorClass() returns a MatrixActor subclass
// - register() factory for DSL compiler output
// - AUTO-PULL context cascade (connectedCallback → findParent → receiveContext)
// - $join handshake, template rendering, microtask move detection
// - Direct child shell discovery stops at shell boundaries (GAP 8 fix)
// - Tracks/autoview support for CQRS view pattern
//
// There is no _isDynamicViewerComponent guard — all MatrixActorHtmlElements
// are first-class actors.

import '../_node-compat';
import type { IMatrixContext } from '../engine/core/IMatrixContext';
import { MatrixActor, type DeepBindingDescriptor } from '../core/MatrixActor';
import { TopicRouter } from '../engine/messaging/TopicRouter';
import type { MxEnvelope } from '../engine/messaging/MxEnvelope';
import { toHandlerCase } from '../engine/utils/NameUtils';
import { RequestReplyHelper } from '../core/messaging/RequestReplyHelper';
import { RequestReply } from '../engine/remoting/RequestReply';
import { ACTOR_SERVICES, type ActorServices } from '../engine/core/ActorServices';
import type { IChildAddedPayload } from '../engine/remoting/IChildAddedPayload';
import type { IChildRemovedPayload } from '../engine/remoting/IChildRemovedPayload';
import type { RootContext } from '../core/context/RealmContext';
import { deriveRootContext } from '../core/context/RealmContext';
import { isTerminalPhase, type ActivityFrame } from './ActivityFrame';
import { STATE_STORE_KEY, type IStateStore } from './IStateStore';
import morphdom from 'morphdom';

// ═══════════════════════════════════════════════════════════════════════════
// Types
// ═══════════════════════════════════════════════════════════════════════════

/** Handler method signature for shell-level handlers. */
type ShellHandler = (payload: unknown, meta: { topic: string; ts: number }) => unknown | Promise<unknown>;

type ReactiveRenderFn = (state: Readonly<Record<string, unknown>>) => string;
type ReactiveRenderCanvasFn = (
  ctx: CanvasRenderingContext2D | null,
  state: Readonly<Record<string, unknown>>
) => void;

interface IReactiveShell {
  render?: ReactiveRenderFn;
  renderCanvas?: ReactiveRenderCanvasFn;
}

interface IActionHandlerPayload {
  event: Event;
  target: Element;
  value?: string;
}

/**
 * Detail payload emitted with mx-capability-request.
 * Used by parent shells to drive escalation UX across composed boundaries.
 */
export interface CapabilityRequestDetail {
  domain: string;
  requestedRight: string;
  component: string;
  action: string;
  error: unknown;
}

type CapabilityErrorLike = Error & {
  domain?: unknown;
  capabilityDomain?: unknown;
  factDomain?: unknown;
  requestedRight?: unknown;
  requiredRight?: unknown;
  right?: unknown;
};

function toCapabilityMessage(err: unknown): string {
  if (err instanceof Error) return err.message;
  if (err && typeof err === 'object' && 'message' in err) {
    const message = (err as { message?: unknown }).message;
    if (typeof message === 'string') return message;
  }
  return String(err);
}

function pickStringField(value: unknown): string | null {
  if (typeof value !== 'string') return null;
  const normalized = value.trim();
  return normalized.length > 0 ? normalized : null;
}

const QUOTED_RIGHT_RE = /\bno\s+['"]([^'"]+)['"]\s+capability\b/i;
const UNQUOTED_RIGHT_RE = /\bno\s+([a-z][a-z0-9_-]*)\s+capability\b/i;
const FACT_DOMAIN_RE = /\bfact\s+domain:\s*([a-z0-9_*{}-]+(?:\.[a-z0-9_*{}-]+)*)/i;
const GENERIC_DOMAIN_RE = /\bdomain:\s*([a-z0-9_*{}-]+(?:\.[a-z0-9_*{}-]+)*)/i;

function extractRequestedRightFromCapabilityError(err: unknown, message: string): string {
  if (err && typeof err === 'object') {
    const capabilityError = err as CapabilityErrorLike;
    const structuredRight =
      pickStringField(capabilityError.requestedRight)
      ?? pickStringField(capabilityError.requiredRight)
      ?? pickStringField(capabilityError.right);
    if (structuredRight) return structuredRight;
  }

  const quotedRight = QUOTED_RIGHT_RE.exec(message);
  if (quotedRight?.[1]) return quotedRight[1];
  const unquotedRight = UNQUOTED_RIGHT_RE.exec(message);
  if (unquotedRight?.[1]) return unquotedRight[1];

  return 'emit';
}

function extractDomainFromCapabilityError(err: unknown, message: string): string {
  if (err && typeof err === 'object') {
    const capabilityError = err as CapabilityErrorLike;
    const structuredDomain =
      pickStringField(capabilityError.domain)
      ?? pickStringField(capabilityError.capabilityDomain)
      ?? pickStringField(capabilityError.factDomain);
    if (structuredDomain) return structuredDomain;
  }

  const factDomain = FACT_DOMAIN_RE.exec(message);
  if (factDomain?.[1]) return factDomain[1];

  const genericDomain = GENERIC_DOMAIN_RE.exec(message);
  if (genericDomain?.[1]) return genericDomain[1];

  return '';
}

function buildCapabilityRequestDetail(
  err: unknown,
  component: string,
  action: string,
): CapabilityRequestDetail {
  const message = toCapabilityMessage(err);
  return {
    domain: extractDomainFromCapabilityError(err, message),
    requestedRight: extractRequestedRightFromCapabilityError(err, message),
    component,
    action,
    error: err,
  };
}

const SANITIZE_PATTERNS: Array<[RegExp, string]> = [
  [/\bon\w+\s*=/i, 'Inline event handler (use data-action)'],
  [/javascript\s*:/i, 'javascript: URI'],
  [/\beval\s*\(/, 'eval() call'],
  [/\bnew\s+Function\s*\(/, 'new Function() call'],
  [/\bdocument\.cookie\b/, 'document.cookie access'],
  [/\b(localStorage|sessionStorage)\b/, 'Browser storage access'],
  [/\b(fetch|XMLHttpRequest)\s*\(/, 'Direct network access'],
  [/new\s+WebSocket\s*\(/, 'WebSocket constructor'],
  [/<\s*(script|iframe|object|embed)\b/i, 'Executable element in template'],
  [/\bsrcdoc\s*=/i, 'srcdoc attribute'],
  [/\bimport\s*\(/, 'Dynamic import()'],
  [/\brequire\s*\(/, 'Dynamic require()'],
];

function rejectKnownBadPatterns(code: string): void {
  for (const [pattern, reason] of SANITIZE_PATTERNS) {
    if (pattern.test(code)) {
      throw new Error(`Component code rejected: ${reason}`);
    }
  }
}

/** Oracle snapshot types (FP-42) — mirrored in packages/agents/src/oracle/types.ts */
export type OracleSnapshotAcceptsEntry = { op: string; params: Array<[string, string]> };
export type OracleSnapshotNode = {
  mount: string;
  tag: string;
  description?: string;
  accepts: OracleSnapshotAcceptsEntry[];
  emits: string[];
  tokens: Record<string, { default: string; description: string }>;
  cognitiveState: Record<string, unknown>;
  children: OracleSnapshotNode[];
};

/** Actor class constructor type. */
export type ActorConstructor = (new () => MatrixActor) & {
  accepts?: Record<string, unknown>;
  emits?: Record<string, unknown>;
  tracks?: string[];
  observedAttributes?: string[];
};

type ContextualChildElement = HTMLElement & {
  _initialProps?: Record<string, unknown>;
  setContext?: (context: IMatrixContext) => void;
  _receiveContext?: (context: IMatrixContext | Record<string, unknown>, existingActor?: MatrixActor) => Promise<void> | void;
};

/** Registration options for register() factory. */
export interface IRegisterOptions {
  /** HTML template string for shadow DOM */
  template?: string;
  /** Observed attributes */
  observedAttributes?: string[];
  /** Tracked component IDs for CQRS view pattern */
  tracks?: string[];
}

// ═══════════════════════════════════════════════════════════════════════════
// MatrixActorHtmlElement
// ═══════════════════════════════════════════════════════════════════════════

export abstract class MatrixActorHtmlElement extends HTMLElement {
  /**
   * Index signature for dynamic handler access.
   * Enables type-safe lookup of onFoo handlers by name.
   */
  [key: string]: unknown;

  /** Commands this element accepts (Pattern A: all-in-one). Override in subclass. */
  static accepts: Record<string, unknown> = {};

  /** Events this element emits (Pattern A: all-in-one). Override in subclass. */
  static emits: Record<string, unknown> = {};

  /**
   * Theme tokens this element declares support for.
   * Each key is a token name (without --mx- prefix), value is metadata.
   * Surfaced in $introspect and oracle snapshots for AI discoverability.
   */
  static tokens: Record<string, { default: string; description: string }> = {};

  /**
   * Human-readable self-description of this component.
   * Surfaced in oracle snapshots so agents know what this component IS,
   * not just what operations it accepts.
   *
   * Override in subclasses:
   *   static description = "Real-time stock ticker for TSLA. Set rules to fire events on price patterns.";
   */
  static description?: string;

  /** Template string for shadow DOM. Override in subclass. */
  static template?: string;

  /** Optional CSS string for reactive render shells. */
  static styles?: string;

  /** Optional initial state seed copied into actor state on mount. */
  static state?: Record<string, unknown>;

  /**
   * Fact domains this component should hydrate and subscribe to on mount.
   *
   * Components can override with:
   *   static factDomains = ['market.tsla', 'portfolio.main'];
   */
  static factDomains: string[] = [];

  /** Tracked component IDs for CQRS view pattern. */
  static tracks?: string[];

  /**
   * Module URL where this element class was defined.
   * Set to import.meta.url for ghost rendering support.
   */
  static moduleUrl?: string;

  /** How long to wait for context before warning (ms). */
  static contextTimeoutMs = 5000;

  /** End-to-end timeout for $prompt round trip (ms). Override per-class or set globally. */
  static promptTimeoutMs = 300_000;

  /**
   * Layout-only flag for transparent container components.
   *
   * When true, child elements skip past this component when searching for
   * their parent shell. The component IS an actor (has accepts/emits, receives
   * context, is network-controllable), but it does NOT parent the elements
   * placed in its light DOM slots. Those children register with the next
   * non-layout ancestor instead, preserving mount paths.
   *
   * Example: <mx-split> owns layout (drag handle, sizing) but doesn't
   * parent the children placed in slot="before"/slot="after".
   */
  static _layoutOnly = false;

  // ═══════════════════════════════════════════════════════════════════════
  // Core fields
  // ═══════════════════════════════════════════════════════════════════════

  /** The internal headless actor (composition-with-delegation). */
  protected _actor: MatrixActor | null = null;

  /** The context received from parent or runtime. */
  protected _context: IMatrixContext | null = null;

  /** Children waiting for context. */
  private _pendingChildren: MatrixActorHtmlElement[] = [];

  /** Whether we've received context. */
  _contextReceived = false;

  /** Whether we're connected to DOM. */
  protected _connected = false;

  /** Pending dispose flag for detecting DOM moves vs removals. */
  protected _pendingDispose = false;

  /** Promise that resolves when element is ready. */
  readonly ready: Promise<void>;
  private _resolveReady!: () => void;

  /** Timeout handle for orphan detection. */
  private _orphanTimeout?: ReturnType<typeof setTimeout>;

  protected get services(): ActorServices {
    const context = this._context;
    if (!context) {
      throw new Error('MATRIX_CONTEXT_NOT_AVAILABLE');
    }

    const services = context.getService<ActorServices>(ACTOR_SERVICES);
    if (!services) {
      throw new Error('ACTOR_SERVICES_NOT_AVAILABLE');
    }

    return services;
  }

  /** Shell-level subscriptions (unsubscribed on context change). */
  private _shellSubs: Array<() => void> = [];

  /** Fact-plane subscriptions (unsubscribed on remount/disconnect). */
  private _factUnsubs: Array<() => void> = [];

  /** Epoch token that invalidates stale async fact hydration work. */
  private _factEpoch = 0;

  /** Remote mount for proxy/clone mode. */
  protected _remoteMount: string | null = null;

  /** Agent session ID for this component — persists across $prompt calls within a page session. */
  protected _agentSessionId: string | null = null;

  /** Accumulated activity frames for the current request (cleared on terminal phase). */
  protected _activityFrames: ActivityFrame[] = [];

  /** Whether activity is currently in progress (between first frame and terminal phase). */
  private _activityActive = false;

  /** Root context for federation (propagated through component tree). */
  private _rootContext: RootContext | null = null;

  /** Context object to propagate to children. */
  private _contextToPropagate: Record<string, unknown> | null = null;

  /** Version counter for factory-created component tags (custom elements can't be re-registered). */
  private readonly _factoryVersions = new Map<string, number>();

  /** Microtask batching gate for render(state) updates. */
  private _renderPending = false;

  /** Guard to avoid re-attaching delegated listeners. */
  private _delegatedEventsWired = false;

  // ═══════════════════════════════════════════════════════════════════════
  // Constructor
  // ═══════════════════════════════════════════════════════════════════════

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.ready = new Promise<void>(resolve => {
      this._resolveReady = resolve;
    });
  }

  protected debugKernel(message: string, ...args: unknown[]): void {
    // eslint-disable-next-line no-console -- kernel browser shell diagnostics have no actor context
    console.debug(message, ...args);
  }

  protected infoKernel(message: string, ...args: unknown[]): void {
    // eslint-disable-next-line no-console -- kernel browser shell diagnostics have no actor context
    console.info(message, ...args);
  }

  protected warnKernel(message: string, ...args: unknown[]): void {
    // eslint-disable-next-line no-console -- kernel browser shell diagnostics have no actor context
    console.warn(message, ...args);
  }

  protected errorKernel(message: string, ...args: unknown[]): void {
    // eslint-disable-next-line no-console -- kernel browser shell diagnostics have no actor context
    console.error(message, ...args);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Static API
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Get the separate actor class for Pattern B.
   * Override in subclass to use a separate actor class.
   * Default: returns null → Pattern A (all-in-one, DynamicActor created).
   */
  static getActorClass(): ActorConstructor | null {
    return null;
  }

  /**
   * Get the custom element tag for creating child DOM elements.
   * Override in subclass to enable auto-creating DOM elements for dynamic children.
   */
  static getChildTag(_componentClassName: string): string | null {
    return null;
  }

  /**
   * Register a custom element for a MatrixActor class (Pattern B shortcut).
   * Used by the DSL compiler to create element wrappers for actor classes.
   *
   * @param tagName - Custom element tag name (e.g., 'my-component')
   * @param ActorClass - MatrixActor subclass
   * @param options - Template, observed attributes
   */
  static register(
    tagName: string,
    ActorClass: ActorConstructor,
    options: IRegisterOptions = {}
  ): void {
    const { template, observedAttributes = [] } = options;

    // Create a concrete element class that wraps the actor
    const ElementClass = class extends MatrixActorHtmlElement {
      static getActorClass(): ActorConstructor { return ActorClass; }
      static template = template;

      static get observedAttributes(): string[] {
        return observedAttributes;
      }
    };

    // Copy accepts/emits from actor class to element class for child event discovery
    const elementCtor = ElementClass as typeof MatrixActorHtmlElement;
    elementCtor.accepts = ActorClass.accepts ?? {};
    elementCtor.emits = ActorClass.emits ?? {};

    // Copy tracks from options or actor class
    const tracks = options.tracks ?? ActorClass.tracks;
    if (tracks) {
      elementCtor.tracks = tracks;
    }

    customElements.define(tagName, ElementClass);
  }

  /**
   * Optional: observed attributes from actor class.
   */
  static get observedAttributes(): string[] {
    try {
      const ActorClass = (this as unknown as typeof MatrixActorHtmlElement).getActorClass?.();
      return ActorClass?.observedAttributes ?? [];
    } catch {
      return [];
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Getters
  // ═══════════════════════════════════════════════════════════════════════

  /** Get the component's name/id. */
  get name(): string {
    return this.getAttribute('name') ?? this.id ?? '';
  }

  /** Get the mount path. */
  get mountPath(): string {
    return this._context?.mount ?? '';
  }

  /** Get the derived mount path from root context. */
  get derivedMount(): string {
    return this._rootContext?.mountPrefix ?? '';
  }

  /** Get the tab-qualified root name. */
  get root(): string {
    return this._rootContext?.root ?? '';
  }

  /** @deprecated Use `root` instead. */
  get realm(): string {
    return this.root;
  }

  /** Get the wrapped actor. */
  get actor(): MatrixActor | null {
    return this._actor;
  }

  /** Backward-compatible getter used by parent shells as mount guard (!el.component). */
  get component(): MatrixActor | null {
    return this._actor;
  }

  /** Reactive state delegate for shells (mirrors MatrixActor.state). */
  get state(): Readonly<Record<string, unknown>> {
    return this._actor?.state ?? {};
  }

  /** Reactive state delegate for shells (mirrors MatrixActor.setState). */
  setState(patch: Partial<Record<string, unknown>>): void {
    if (!this._actor) {
      // Don't throw — generated components (clocks, etc.) may call setState
      // from setInterval before mount completes or after unmount.
      this.warnKernel('setState called before mount — ignored', patch);
      return;
    }
    this._actor.setState(patch);
  }

  /** Backward-compatible alias for ready promise (FabricElement compat). */
  get contextReady(): Promise<void> {
    return this.ready;
  }

  /** Check if this element is in proxy/clone mode. */
  get isProxyMode(): boolean {
    return this._remoteMount !== null;
  }

  /** Get the remote mount path (if in proxy/clone mode). */
  get remoteMount(): string | null {
    return this._remoteMount;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // DOM Lifecycle
  // ═══════════════════════════════════════════════════════════════════════

  connectedCallback(): void {
    this._connected = true;
    this._pendingDispose = false;

    // Ghost shells are mounted explicitly by ComponentProxy.render()
    const isGhostShell = this.hasAttribute('data-remote-mount');
    if (isGhostShell) {
      this._startOrphanDetection();
      return;
    }

    // Re-attach after a disconnect/move: actor/context are still live, so
    // re-arm fact lifecycle subscriptions immediately.
    if (this._contextReceived && this._context && this._actor) {
      this._startFactLifecycle();
      return;
    }

    const parentShell = this._findParentShell();

    if (parentShell && this._isExplicitExternalHost(parentShell)) {
      return;
    }

    if (parentShell) {
      parentShell._registerPendingChild(this);
    } else if (this._isDirectChildOfMatrixDslHost()) {
      return;
    } else {
      // Root element — wait for explicit mount(context)
      this._startOrphanDetection();
    }
  }

  disconnectedCallback(): void {
    this._connected = false;
    this._clearOrphanTimeout();
    this._clearFactSubs();

    // ═══════════════════════════════════════════════════════════════════════
    // MICROTASK MOVE DETECTION (validated by LIT/Polymer team)
    // Defers disposal to detect moves (disconnect → reconnect).
    // ═══════════════════════════════════════════════════════════════════════
    this._pendingDispose = true;
    queueMicrotask(() => {
      if (!this._connected && this._pendingDispose) {
        // Send $leave to parent — local fast-path when possible
        if (this._context && this._context.parent) {
          const leavePayload = { id: this.name, mount: this._context.mount };
          const parentShell = this._findParentShell();
          if (parentShell?._actor && typeof parentShell._actor.on$leave === 'function') {
            parentShell._actor.on$leave(leavePayload);
          } else {
            const parentInbox = TopicRouter.inbox(this._context.parent.mount);
            const leaveEnvelope: MxEnvelope = {
              op: '$leave',
              payload: leavePayload,
            };
            this._context.transport.publish(parentInbox, leaveEnvelope);
          }
        }
        this._clearShellSubs();
        this._actor?.dispose();
        this._actor = null;
        this._context = null;
        this._contextToPropagate = null;
        this._rootContext = null;
        this._contextReceived = false;
      }
    });
  }

  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
    if (this._actor && oldValue !== newValue) {
      // Bridge attribute changes to actor handler
      const handlerName = `on${toHandlerCase(name)}Changed`;
      const actorRecord = this._actor as unknown as Record<string, unknown>;
      const handler = actorRecord[handlerName];
      if (typeof handler === 'function') {
        handler.call(this._actor, newValue, oldValue);
      }
    }

    // Also check for handler on element itself (Pattern A)
    if (oldValue !== newValue) {
      const handlerName = `on${toHandlerCase(name)}Changed`;
      const handler = this[handlerName];
      if (typeof handler === 'function') {
        (handler as Function).call(this, newValue, oldValue);
      }
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Context Management
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Receive context from parent or runtime.
   * Triggers actor creation and bootstrap.
   */
  async _receiveContext(context: IMatrixContext | Record<string, unknown>, existingActor?: MatrixActor): Promise<void> {
    // Handle root context propagation from MatrixDSLHost
    // Backward compat: check both 'rootContext' and legacy 'realmContext'
    if (context && ('rootContext' in context || 'realmContext' in context) && !('transport' in context)) {
      const parentContext = (
        ('rootContext' in context ? context.rootContext : undefined) ??
        ('realmContext' in context ? context.realmContext : undefined)
      ) as RootContext;
      const mount = this.getAttribute('mount');
      this._rootContext = deriveRootContext(parentContext, mount ?? undefined);

      this._contextToPropagate = {
        ...context,
        rootContext: this._rootContext,
        realmContext: this._rootContext, // backward compat
      };

      // Propagate to pending children
      for (const child of this._pendingChildren) {
        if (typeof child._receiveContext === 'function') {
          void child._receiveContext(this._contextToPropagate);
        }
      }
      return;
    }

    const matrixContext = context as IMatrixContext;

    // Check for proxy/clone mode
    this._remoteMount = this.getAttribute('data-remote-mount');

    // Element instances can be detached/disposed and later re-used.
    // If actor/context are gone, drop the stale mounted flag and run full bootstrap.
    if (this._contextReceived && !this._actor) {
      this._contextReceived = false;
      this._context = null;
      this._clearShellSubs();
    }

    // Remount handling
    if (
      this._contextReceived
      && this._actor
      && (this._context !== matrixContext || this._actor.context !== matrixContext)
    ) {
      this._context = matrixContext;
      this._clearFactSubs();
      if (this._actor) {
        this._actor._shellUpdateCallback = () => this._scheduleRender();
        await this._actor.initialize(matrixContext, this.name);
      }
      this._wireDelegatedEvents();
      this._clearShellSubs();
      this._wireShellAccepts();
      this._wireShellTracks();
      this._enableAutoView();
      this._subscribeToDynamicChildren();
      await this._onContextUpdated(matrixContext);
      this._startFactLifecycle();
      if (this._hasRenderMethod()) {
        this._scheduleRender();
      }
      return;
    }

    if (this._contextReceived) return;

    this._contextReceived = true;
    this._clearOrphanTimeout();
    this._context = matrixContext;

    try {
      // Create actor
      if (existingActor) {
        this._actor = existingActor;
      } else {
        this._actor = this._createActor();
      }

      // Inject shell info for introspect + source code
      const ctor = this.constructor as typeof MatrixActorHtmlElement;
      this._actor._shellInfo = {
        shellTag: this.tagName.toLowerCase(),
        moduleUrl: ctor.moduleUrl,
        description: ctor.description,
      };
      this._actor._shellElement = this;
      this._actor._shellUpdateCallback = () => this._scheduleRender();
      this._wireDelegatedEvents();

      // Bridge initial attributes
      this._bridgeAttributes();

      // Render template
      await this._renderTemplate();

      // Seed static initial state after template exists (no emit, no callback).
      if (!existingActor) {
        this._applyStaticStateSeed();
      }

      // Kick the first reactive render only for components that define render(state).
      if (this._hasRenderMethod()) {
        this._scheduleRender();
      }

      // Fire-and-forget KV hydration: reads persisted state from JetStream KV via daemon,
      // merges over static seed, and re-renders. Non-blocking — component is usable immediately.
      if (!existingActor) {
        this._hydrateFromKv();
      }

      // Initialize actor
      if (!existingActor) {
        await this._actor.initialize(matrixContext, this.name);
      }

      // $join handshake — local fast-path when parent is in the same JS context
      const parentMount = matrixContext.parent?.mount;
      if (parentMount) {
        const parentShell = this._findParentShell();
        const joinPayload = {
          id: this.name,
          mount: matrixContext.mount,
          componentClass: this._actor.constructor.name,
          props: {},
        };

        if (parentShell?._actor && typeof parentShell._actor.on$join === 'function') {
          // LOCAL FAST-PATH: parent is in the same browser tab — skip NATS round-trip.
          // Call on$join directly to register this child. No network I/O needed.
          parentShell._actor.on$join(joinPayload);
        } else {
          // REMOTE PATH: parent is on a different process/root — use NATS transport.
          const parentInbox = TopicRouter.inbox(parentMount);
          const myInbox = TopicRouter.inbox(matrixContext.mount);

          const ackPromise = new Promise<void>((resolve, reject) => {
            const timeout = setTimeout(() => {
              sub();
              reject(new Error(`Timeout waiting for $join_ack from ${matrixContext.parent!.mount}`));
            }, 5000);

            const sub = matrixContext.transport.subscribe(myInbox, (raw: unknown) => {
              const envelope = raw as MxEnvelope;
              if (envelope?.op !== '$join_ack') return;
              clearTimeout(timeout);
              sub();
              resolve();
            });
          });

          const joinEnvelope: MxEnvelope = {
            op: '$join',
            payload: joinPayload,
          };
          matrixContext.transport.publish(parentInbox, joinEnvelope);

          await ackPromise;
        }
      }
      // Wire shell-level accepts handlers (Pattern A)
      this._wireShellAccepts();

      // Wire self-tracking: shell subscribes to own actor's $events (Pattern B)
      this._wireSelfTracking();

      // Wire tracks for CQRS view pattern
      this._wireShellTracks();

      // Enable autoview if tracks + autoview attribute set
      this._enableAutoView();

      // Wire children
      await this._wireChildren();

      // Subscribe to dynamic child lifecycle
      this._subscribeToDynamicChildren();

      // Hook for subclass
      await this._onContextUpdated(matrixContext);

      // Start fact-plane hydration/subscription after static state bootstrap.
      this._startFactLifecycle();

      // Wire static subscribes — auto-dispatch daemon events to handlers
      this._wireStaticSubscribes();

      // Optional bootstrap hook for subclasses
      await this.onBootstrap();

      // Mark ready
      this._resolveReady();
      this.setAttribute('data-matrix-ready', '');

    } catch (err) {
      this.errorKernel(`[MatrixActorHtmlElement] Error during context receive ($join failed):`, err);
      throw err;
    }
  }

  /**
   * Provide context to this element (called by runtime for root elements).
   */
  async mount(context: IMatrixContext): Promise<void> {
    await this._receiveContext(context);
  }

  /**
   * Backward-compatible API for setting context (FabricElement compat).
   */
  setContext(context: IMatrixContext): void {
    this.mount(context).catch(err => {
      this.errorKernel(`[MatrixActorHtmlElement] Error in setContext/mount:`, err);
    });
  }

  /**
   * Register a pending child element.
   */
  _registerPendingChild(child: MatrixActorHtmlElement): void {
    // Skip if child already has context
    if (child._contextReceived) return;

    if (this._contextReceived && this._context) {
      const childContext = this._context.derive((child.name || child.id || child.tagName).toLowerCase());
      // Subscribe to child events BEFORE giving context (subscribe-before-context invariant)
      this._subscribeToChildEvents(child, childContext);
      child._receiveContext(childContext);
    } else {
      this._pendingChildren.push(child);
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Actor Creation
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Create the internal actor instance.
   * Pattern B: uses getActorClass() to get a separate actor class.
   * Pattern A: creates a DynamicActor with accepts/emits from the element class.
   */
  private _createActor(): MatrixActor {
    const ctor = this.constructor as typeof MatrixActorHtmlElement;
    const ActorClass = ctor.getActorClass();

    if (ActorClass) {
      // Pattern B: separate actor class
      return new ActorClass();
    }

    // Pattern A: create DynamicActor from element's static accepts/emits
    const shellAccepts = ctor.accepts ?? {};
    const shellEmits = ctor.emits ?? {};
    const shellDescription = ctor.description;

    class DynamicActor extends MatrixActor {
      static accepts = { ...shellAccepts };
      static emits = { ...shellEmits };
      static description = shellDescription;
    }

    return new DynamicActor();
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Template Rendering
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Render template to shadow DOM.
   * Checks element class first, then actor class.
   */
  protected async _renderTemplate(): Promise<void> {
    if (!this.shadowRoot) return;

    const { template, styles } = this._resolveTemplateAndStyles();

    // Backward compatibility: old imperative components keep one-shot template rendering.
    if (!this._hasRenderMethod()) {
      if (!template) return;
      const tpl = document.createElement('template');
      tpl.innerHTML = String(template).trim();
      await this._waitForCustomElements(tpl.content);
      this.shadowRoot.replaceChildren(...Array.from(tpl.content.childNodes));
      return;
    }

    // Reactive layout: [style zone] + [morphdom render zone] + [child host zone].
    const styleEl = document.createElement('style');
    styleEl.setAttribute('data-mx-style', '');
    styleEl.textContent = styles ? String(styles) : '';

    const renderRoot = document.createElement('div');
    renderRoot.setAttribute('data-mx-root', '');

    if (template) {
      const tpl = document.createElement('template');
      tpl.innerHTML = String(template).trim();
      await this._waitForCustomElements(tpl.content);
      renderRoot.replaceChildren(...Array.from(tpl.content.childNodes));
    }

    this.shadowRoot.replaceChildren(styleEl, renderRoot);
  }

  /**
   * Wait for custom elements in a node to be defined.
   */
  private async _waitForCustomElements(node: Node): Promise<void> {
    const elements = (node as Element).querySelectorAll?.('*') ?? [];
    const tags = new Set<string>();

    for (const el of Array.from(elements)) {
      if (el.tagName.includes('-')) {
        tags.add(el.tagName.toLowerCase());
      }
    }

    await Promise.all(
      Array.from(tags).map(tag =>
        customElements.whenDefined(tag).catch(() => {})
      )
    );
  }

  /**
   * Schedule a reactive render using microtask batching.
   * Multiple setState()/emit() calls in one sync turn collapse into one DOM diff.
   */
  private _scheduleRender(): void {
    if (this._renderPending) return;
    this._renderPending = true;

    queueMicrotask(() => {
      this._renderPending = false;

      const html = this._evaluateRender();
      if (html === null || !this.shadowRoot) return;

      const renderRoot = this.shadowRoot.querySelector('[data-mx-root]');
      if (!renderRoot) return;

      const wrapper = document.createElement('div');
      wrapper.innerHTML = html;

      const sr = this.shadowRoot;
      morphdom(renderRoot, wrapper, {
        childrenOnly: true,
        onBeforeElUpdated(fromEl: Element, toEl: Element): boolean {
          if (fromEl === sr.activeElement) return false;
          if (fromEl.isEqualNode(toEl)) return false;
          return true;
        },
      });

      this._renderCanvas();
    });
  }

  /**
   * Evaluate render(state) if present.
   * Returns null for legacy components with no render() method.
   */
  private _evaluateRender(): string | null {
    const reactiveShell = this as MatrixActorHtmlElement & IReactiveShell;
    if (typeof reactiveShell.render !== 'function') {
      return null;
    }
    const rendered = reactiveShell.render(this.state);
    return rendered == null ? '' : String(rendered);
  }

  /**
   * Event delegation for data-action handlers so morphdom updates never orphan listeners.
   */
  private _wireDelegatedEvents(): void {
    if (!this.shadowRoot || this._delegatedEventsWired) return;
    this._delegatedEventsWired = true;

    const dispatch = (event: Event): void => {
      const path = event.composedPath();
      const rawTarget = (path[0] ?? event.target) as EventTarget | null;

      let origin: Element | null = null;
      if (rawTarget instanceof Element) {
        origin = rawTarget;
      } else if (rawTarget instanceof Node) {
        origin = rawTarget.parentElement;
      }
      if (!origin) return;

      const actionTarget = origin.closest('[data-action]');
      if (!(actionTarget instanceof Element)) return;
      if (!this.shadowRoot?.contains(actionTarget)) return;

      const action = actionTarget.getAttribute('data-action');
      if (!action) return;

      const handlerName = `on${toHandlerCase(action)}`;
      const handler = this[handlerName];
      if (typeof handler !== 'function') return;

      const value = actionTarget instanceof HTMLInputElement
        ? actionTarget.value
        : actionTarget instanceof HTMLTextAreaElement
          ? actionTarget.value
          : actionTarget instanceof HTMLSelectElement
            ? actionTarget.value
            : undefined;

      const handleErr = (err: unknown): void => {
        const message = toCapabilityMessage(err);
        if (/\bcapability\b/i.test(message)) {
          const componentTag = this.localName || this.tagName.toLowerCase();
          const detail = buildCapabilityRequestDetail(err, componentTag, action);
          this.dispatchEvent(new CustomEvent<CapabilityRequestDetail>('mx-capability-request', {
            bubbles: true,
            composed: true,
            detail,
          }));
          return;
        }
        this.errorKernel(`[${this.tagName.toLowerCase()}] Unhandled error in ${handlerName}:`, err);
      };

      try {
        const result = (handler as (payload: IActionHandlerPayload) => unknown).call(this, {
          event,
          target: actionTarget,
          value,
        });
        if (result && typeof (result as PromiseLike<unknown>).then === 'function') {
          void Promise.resolve(result).catch(handleErr);
        }
      } catch (err) {
        handleErr(err);
      }
    };

    this.shadowRoot.addEventListener('click', dispatch);
    this.shadowRoot.addEventListener('input', dispatch);
    this.shadowRoot.addEventListener('change', dispatch);
    this.shadowRoot.addEventListener('submit', (event) => {
      event.preventDefault();
      dispatch(event);
    });
    this.shadowRoot.addEventListener('focusin', dispatch);
    this.shadowRoot.addEventListener('focusout', dispatch);
  }

  private _renderCanvas(): void {
    if (!this.shadowRoot) return;
    const reactiveShell = this as MatrixActorHtmlElement & IReactiveShell;
    if (typeof reactiveShell.renderCanvas !== 'function') return;

    const canvas = this.shadowRoot.querySelector('canvas');
    if (!(canvas instanceof HTMLCanvasElement)) return;

    reactiveShell.renderCanvas(canvas.getContext('2d'), this.state);
  }

  private _resolveTemplateAndStyles(): { template: string | null; styles: string | null } {
    const ctor = this.constructor as typeof MatrixActorHtmlElement;
    let template = ctor.template ?? null;
    let styles = ctor.styles ?? null;

    // Fall back to actor class static template/styles (Pattern B).
    const ActorClass = ctor.getActorClass();
    if (ActorClass) {
      const actorStatics = ActorClass as unknown as { template?: string; styles?: string };
      template ??= actorStatics.template ?? null;
      styles ??= actorStatics.styles ?? null;
    }

    return { template, styles };
  }

  private _hasRenderMethod(): boolean {
    const reactiveShell = this as MatrixActorHtmlElement & IReactiveShell;
    return typeof reactiveShell.render === 'function';
  }

  private _applyStaticStateSeed(): void {
    if (!this._actor) return;

    const ctor = this.constructor as typeof MatrixActorHtmlElement & {
      state?: Record<string, unknown>;
    };
    const initialState = ctor.state;
    if (!initialState || typeof initialState !== 'object') return;

    const actorWithState = this._actor as MatrixActor & { _state: Record<string, unknown> };
    let cloned = initialState;
    try {
      cloned = this._cloneState(initialState);
    } catch {
      cloned = { ...initialState };
    }
    Object.assign(actorWithState._state, cloned);
  }

  private _cloneState<T>(value: T): T {
    if (typeof structuredClone === 'function') {
      return structuredClone(value);
    }
    return JSON.parse(JSON.stringify(value)) as T;
  }

  /**
   * Fire-and-forget: reads persisted state from the context's state-store service.
   * Merges KV state over the static seed and triggers re-render via morphdom.
   * Only runs for reactive components (those with render(state)) that haven't opted out.
   *
   * The state-store service is environment-agnostic — browser contexts get a
   * RemoteStateStore (RequestReply to daemon), daemon contexts get direct KV access.
   * Components never know or care where persisted state lives.
   */
  private _hydrateFromKv(): void {
    const shellCtor = this.constructor as typeof MatrixActorHtmlElement & { persistState?: boolean };
    const actorCtor = this._actor?.constructor as { persistState?: boolean } | undefined;
    const persist = shellCtor.persistState ?? actorCtor?.persistState ?? true;
    if (persist === false) {
      this.debugKernel(`[KV-Hydrate] ${this._context?.mount ?? '?'} — skipped (persistState=false)`);
      return;
    }
    if (!this._hasRenderMethod()) {
      this.debugKernel(`[KV-Hydrate] ${this._context?.mount ?? '?'} — skipped (no render method)`);
      return;
    }

    const actor = this._actor;
    const context = this._context;
    if (!actor || !context) {
      this.debugKernel(`[KV-Hydrate] skipped — no actor or context`);
      return;
    }

    const stateStore = context.getService<IStateStore>(STATE_STORE_KEY);
    if (!stateStore) {
      this.debugKernel(`[KV-Hydrate] ${context.mount} — skipped (no state-store service)`);
      return;
    }

    const mount = context.mount;
    const sessionId = context.getService<string>('session-id');
    this.debugKernel(`[KV-Hydrate] ${mount} — reading from state-store (sessionId=${sessionId ?? 'none'})`);

    stateStore.read(mount, sessionId ?? undefined)
      .then((state) => {
        if (state) {
          const actorWithState = actor as MatrixActor & { _state: Record<string, unknown> };
          Object.assign(actorWithState._state, state);
          this.debugKernel(`[KV-Hydrate] ${mount} — merged state:`, JSON.stringify(state).slice(0, 200));
          this._scheduleRender();
        } else {
          this.debugKernel(`[KV-Hydrate] ${mount} — no persisted state found`);
        }
      })
      .catch((err) => {
        this.debugKernel(`[KV-Hydrate] ${mount} — error: ${err instanceof Error ? err.message : String(err)}`);
      });
  }

  /**
   * Called during mount for each declared fact domain with existing data.
   * Override in subclasses to merge snapshot data into component state.
   */
  protected onFactLoaded(_domain: string, _data: unknown, _revision: number): void | Promise<void> {
    return;
  }

  /**
   * Called when a subscribed fact domain receives an update after mount.
   * Override in subclasses to react to streaming fact changes.
   */
  protected onFactUpdated(_domain: string, _data: unknown): void | Promise<void> {
    return;
  }

  /**
   * Resolve and sanitize the declared fact domains for this component class.
   */
  private _resolveFactDomains(): string[] {
    const ctor = this.constructor as typeof MatrixActorHtmlElement;
    const domains = Array.isArray(ctor.factDomains) ? ctor.factDomains : [];
    return [...new Set(domains.map(domain => String(domain).trim()).filter(Boolean))];
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Daemon-Scoped Subscription Helper
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Subscribe to a daemon-scoped topic without needing to know the daemon root.
   *
   * ```ts
   * this.subscribeToDaemon('harness/$events', (payload) => { ... });
   * this.subscribeToDaemon('system.agents/$events', (payload) => { ... });
   * ```
   *
   * Equivalent to:
   * ```ts
   * const root = this._resolveDaemonRoot();
   * this._context.transport.subscribe(`${root}/${topic}`, handler);
   * ```
   */
  protected subscribeToDaemon(topic: string, handler: (payload: unknown) => void): () => void {
    const transport = this._context?.transport;
    if (!transport) {
      // eslint-disable-next-line no-console
      console.warn(`[subscribeToDaemon] No transport available for topic: ${topic}`);
      return () => {};
    }
    const daemonRoot = this._resolveDaemonRoot();
    const resolved = `${daemonRoot}/${topic}`;
    return transport.subscribe(resolved, handler);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Static Subscribes — declarative daemon event subscription
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Wire `static subscribes` declarations to handler methods.
   *
   * Components declare what daemon events they want:
   * ```ts
   * static subscribes = {
   *   'system.harness.loop/$events': {
   *     filter: ['harness.iterationComplete', 'harness.frontierUpdated'],
   *     description: 'Live updates from the optimization loop',
   *   },
   * };
   * ```
   *
   * The framework:
   * 1. Resolves the daemon root (no hardcoding needed)
   * 2. Subscribes via cross-root addressing
   * 3. Dispatches to `on{OpName}` handlers (same convention as `static accepts`)
   * 4. Applies optional filters
   */
  private _wireStaticSubscribes(): void {
    if (!this._context?.transport) return;

    const ctor = this.constructor as unknown as Record<string, unknown>;
    const subscribes = ctor.subscribes as Record<string, {
      filter?: string[];
      description?: string;
    }> | undefined;

    if (!subscribes || typeof subscribes !== 'object') return;

    const daemonRoot = this._resolveDaemonRoot();

    for (const [topic, config] of Object.entries(subscribes)) {
      // Cross-root addressing: 'system.harness.loop/$events'
      // → '{daemonRoot}/system.harness.loop/$events'
      const resolved = `${daemonRoot}/${topic}`;

      this._context.transport.subscribe(resolved, (envelope: unknown) => {
        const env = envelope as { op?: string; payload?: unknown };
        const op = env?.op;
        if (!op) return;

        // Apply filter if specified
        if (config.filter && !config.filter.includes(op)) return;

        // Convert op to handler name:
        //   'harness.iterationComplete' → 'onHarnessIterationComplete'
        const handlerName = 'on' + op.split('.').map(
          (segment: string) => segment.charAt(0).toUpperCase() + segment.slice(1),
        ).join('');

        const handler = (this as Record<string, unknown>)[handlerName];
        if (typeof handler === 'function') {
          try {
            handler.call(this, env.payload);
          } catch (err) {
            // eslint-disable-next-line no-console
            console.error(`[StaticSubscribes] ${handlerName} threw:`, err);
          }
        }
      });
    }
  }

  /**
   * Start mount-time fact hydration and live subscription wiring.
   */
  private _startFactLifecycle(): void {
    if (!this._actor || !this._context) return;

    const domains = this._resolveFactDomains();
    if (domains.length === 0) return;

    this._clearFactSubs();
    const epoch = this._factEpoch;
    const hasPushSubscribe = typeof this._context.subscribeFact === 'function';

    if (hasPushSubscribe) {
      for (const domain of domains) {
        try {
          const unsub = this._actor.subscribeFact(domain, (data: unknown) => {
            if (epoch !== this._factEpoch) return;
            void this._invokeFactUpdated(domain, data);
          });
          this._factUnsubs.push(unsub);
        } catch (err) {
          this.warnKernel(`[MatrixActorHtmlElement] Failed to subscribe fact '${domain}'`, err);
        }
      }

      void this._hydrateFacts(domains, epoch);
      return;
    }

    void this._hydrateFacts(domains, epoch).then((baselineRevisions) => {
      if (epoch !== this._factEpoch) return;
      for (const domain of domains) {
        try {
          const baseline = baselineRevisions.get(domain) ?? null;
          const unsub = this._startFactPolling(domain, epoch, baseline);
          this._factUnsubs.push(unsub);
        } catch (err) {
          this.warnKernel(`[MatrixActorHtmlElement] Failed to start fact polling for '${domain}'`, err);
        }
      }
    });
  }

  /**
   * Read initial fact snapshots asynchronously after mount.
   */
  private async _hydrateFacts(domains: readonly string[], epoch: number): Promise<Map<string, number | null>> {
    const actor = this._actor;
    const revisions = new Map<string, number | null>();
    if (!actor) return revisions;

    await Promise.all(domains.map(async (domain) => {
      if (epoch !== this._factEpoch) return;
      try {
        const fact = await actor.readFact(domain);
        if (!fact) {
          revisions.set(domain, null);
          return;
        }
        if (epoch !== this._factEpoch) return;
        revisions.set(domain, fact.revision);
        await this._invokeFactLoaded(domain, fact.data, fact.revision);
      } catch (err) {
        revisions.set(domain, null);
        this.warnKernel(`[MatrixActorHtmlElement] Failed to hydrate fact '${domain}'`, err);
      }
    }));

    return revisions;
  }

  /**
   * Poll for fact changes when the context has no push subscription hook.
   * Uses hydration revision as baseline to avoid duplicate mount-time updates.
   */
  private _startFactPolling(domain: string, epoch: number, baselineRevision: number | null): () => void {
    const actor = this._actor;
    if (!actor) {
      return () => {};
    }

    let active = true;
    let inFlight = false;
    let lastRevision = baselineRevision;

    const poll = async (): Promise<void> => {
      if (!active || inFlight || epoch !== this._factEpoch) return;
      inFlight = true;

      try {
        const fact = await actor.readFact(domain);
        if (!active || epoch !== this._factEpoch || !fact) return;

        if (lastRevision === null) {
          lastRevision = fact.revision;
          await this._invokeFactUpdated(domain, fact.data);
          return;
        }

        if (fact.revision !== lastRevision) {
          lastRevision = fact.revision;
          await this._invokeFactUpdated(domain, fact.data);
        }
      } catch (err) {
        this.warnKernel(`[MatrixActorHtmlElement] Failed to poll fact '${domain}'`, err);
      } finally {
        inFlight = false;
      }
    };

    const intervalId = setInterval(() => { void poll(); }, 3000);
    if (
      typeof intervalId === 'object' &&
      intervalId !== null &&
      'unref' in intervalId &&
      typeof intervalId.unref === 'function'
    ) {
      intervalId.unref();
    }

    return () => {
      active = false;
      clearInterval(intervalId);
    };
  }

  /**
   * Invoke the subclass fact-loaded hook with error isolation.
   */
  private async _invokeFactLoaded(domain: string, data: unknown, revision: number): Promise<void> {
    try {
      await this.onFactLoaded(domain, data, revision);
    } catch (err) {
      this.errorKernel(`[MatrixActorHtmlElement] onFactLoaded failed for '${domain}'`, err);
    }
  }

  /**
   * Invoke the subclass fact-updated hook with error isolation.
   */
  private async _invokeFactUpdated(domain: string, data: unknown): Promise<void> {
    try {
      await this.onFactUpdated(domain, data);
    } catch (err) {
      this.errorKernel(`[MatrixActorHtmlElement] onFactUpdated failed for '${domain}'`, err);
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Shell-level Accept Wiring (Pattern A)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Wire shell accept handlers to transport topics.
   * Handlers on element class (onSetData, onSortBy, etc.) receive messages.
   * Only applies for Pattern A (all-in-one) where handlers live on the element.
   */
  protected _wireShellAccepts(): void {
    if (!this._context) return;

    const ctor = this.constructor as typeof MatrixActorHtmlElement;
    const isPatternB = !!ctor.getActorClass();

    // Framework ops — always available on every HTML element (like $introspect).
    // Map op name → handler name directly because $ prefix or dots break toHandlerCase.
    const frameworkOps: Record<string, string> = {
      '$style': 'on$Style',
      '$tokens': 'on$Tokens',
      '$prompt': 'on$prompt',
      '$cancel': 'on$Cancel',
      '$activity': 'on$Activity',
      'snapshot.tree.oracle': 'onSnapshotTreeOracle',
      'factory.create-from-code': 'onFactoryCreateFromCode',
      'factory.load': 'onFactoryLoad',
      'factory.create-from-url': 'onFactoryCreateFromUrl',
      'component.save': 'onComponentSave',
    };

    // For Pattern B, only wire framework ops (actor handles domain accepts).
    // For Pattern A, wire both domain accepts and framework ops.
    const accepts = isPatternB ? [] : Object.keys(ctor.accepts ?? {});
    const mount = this._context.mount;
    const transport = this._context.transport;

    // Build dispatch table for accepts + framework ops
    const dispatch: Record<string, ShellHandler> = {};
    for (const cmd of accepts) {
      const handlerName = `on${toHandlerCase(cmd)}`;
      const handler = this[handlerName];
      if (typeof handler === 'function') {
        dispatch[cmd] = handler as ShellHandler;
      }
    }
    // Wire framework ops with explicit handler names ($ prefix breaks toHandlerCase)
    for (const [op, handlerName] of Object.entries(frameworkOps)) {
      const handler = this[handlerName];
      if (typeof handler === 'function') {
        dispatch[op] = handler as ShellHandler;
      }
    }

    // Nothing to wire
    if (Object.keys(dispatch).length === 0) return;

    // Single $inbox subscription with op-based dispatch
    const inboxTopic = TopicRouter.inbox(mount);
    const unsub = transport.subscribe(inboxTopic, async (payload, meta) => {
      const envelope = payload as MxEnvelope;
      if (!envelope?.op) return;
      const handler = dispatch[envelope.op];
      if (!handler) return;

      try {
        const hasReply = !!(envelope.replyTo && envelope.correlationId);
        const payloadForRR: unknown = hasReply
          ? { correlationId: envelope.correlationId, replyTo: envelope.replyTo, params: envelope.payload }
          : envelope.payload;
        await RequestReplyHelper.executeWithReplySafe(
          transport,
          payloadForRR,
          async (params) => handler.call(this, params ?? envelope.payload, meta)
        );
      } catch (err) {
        this.errorKernel(`[MatrixActorHtmlElement] Error in handler for '${envelope.op}':`, err);
      }
    });

    this._addShellSub(unsub);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Self-Tracking (Pattern B: actor emits → shell handlers)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Wire self-tracking for Pattern B shells.
   * Subscribes to the actor's own $events topic and dispatches
   * to shell on<Event>() handlers (e.g., onFlowCompleted, onExampleLoaded).
   */
  private _wireSelfTracking(): void {
    if (!this._context) return;

    const ctor = this.constructor as typeof MatrixActorHtmlElement;
    // Only for Pattern B — Pattern A has no separate actor
    if (!ctor.getActorClass()) return;

    const mount = this._context.mount;
    const transport = this._context.transport;

    // Build dispatch table from actor's emits → shell handler methods
    const actorEmits = ctor.getActorClass()?.emits ?? {};
    const dispatch: Record<string, Function> = {};
    for (const eventName of Object.keys(actorEmits)) {
      const handlerName = `on${toHandlerCase(eventName)}`;
      const handler = this[handlerName];
      if (typeof handler === 'function') {
        dispatch[eventName] = handler.bind(this);
      }
    }

    if (Object.keys(dispatch).length === 0) return;

    // Single subscription to actor's $events
    const eventsTopic = TopicRouter.events(mount);
    const unsub = transport.subscribe(eventsTopic, async (payload, meta) => {
      const envelope = payload as MxEnvelope;
      if (!envelope?.op) return;
      const handler = dispatch[envelope.op];
      if (handler) {
        try {
          await handler(envelope.payload, meta);
        } catch (err) {
          this.errorKernel(`[MatrixActorHtmlElement] Self-track handler error '${envelope.op}':`, err);
        }
      }
    });
    this._addShellSub(unsub);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Tracks Wiring (CQRS View Pattern)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Wire shell track handlers to tracked component events.
   * Sources: HTML tracks attribute + static tracks property.
   * Handler discovery: on<Event>() and on<Event>From<Id>() methods.
   */
  private _wireShellTracks(): void {
    if (!this._context) return;

    const tracksAttr = this.getAttribute('tracks');
    const ctor = this.constructor as typeof MatrixActorHtmlElement;
    const staticTracks = ctor.tracks;

    const trackIds = [
      ...(tracksAttr ? tracksAttr.split(',').map(id => id.trim()) : []),
      ...(staticTracks ?? []),
    ].filter(Boolean);

    const uniqueTrackIds = [...new Set(trackIds)];
    if (uniqueTrackIds.length === 0) return;

    const myMount = this._context.mount;
    const parentMount = myMount.split('.').slice(0, -1).join('.');
    const transport = this._context.transport;
    const handlerMethods = this._discoverHandlerMethods();

    for (const trackedId of uniqueTrackIds) {
      const childMount = myMount ? `${myMount}.${trackedId}` : trackedId;
      const siblingMount = parentMount ? `${parentMount}.${trackedId}` : trackedId;

      const trackedIdNorm = toHandlerCase(trackedId);

      // Build dispatch table for this tracked component
      const trackHandlers: Record<string, Function> = {};
      for (const { eventName, handler, isSpecific, targetId } of handlerMethods) {
        if (isSpecific && targetId !== trackedIdNorm) continue;
        trackHandlers[eventName] = handler;
      }

      if (Object.keys(trackHandlers).length === 0) continue;

      // SINGLE subscription to tracked component's $events
      const childEventsTopic = TopicRouter.events(childMount);
      const unsub1 = transport.subscribe(childEventsTopic, async (payload, meta) => {
        const envelope = payload as MxEnvelope;
        if (!envelope || !envelope.op) return;
        const handler = trackHandlers[envelope.op];
        if (handler) {
          try {
            await handler(envelope.payload, meta);
          } catch (err) {
            this.errorKernel(`[MatrixActorHtmlElement] Track handler error '${envelope.op}':`, err);
          }
        }
      });
      this._addShellSub(unsub1);

      // Subscribe at sibling mount too, if different
      if (childMount !== siblingMount) {
        const siblingEventsTopic = TopicRouter.events(siblingMount);
        const unsub2 = transport.subscribe(siblingEventsTopic, async (payload, meta) => {
          const envelope = payload as MxEnvelope;
          if (!envelope || !envelope.op) return;
          const handler = trackHandlers[envelope.op];
          if (handler) {
            try {
              await handler(envelope.payload, meta);
            } catch (err) {
              this.errorKernel(`[MatrixActorHtmlElement] Track handler error '${envelope.op}':`, err);
            }
          }
        });
        this._addShellSub(unsub2);
      }
    }
  }

  /**
   * Discover handler methods using naming convention.
   */
  private _discoverHandlerMethods(): Array<{
    eventName: string;
    handler: ShellHandler;
    isSpecific: boolean;
    targetId?: string;
  }> {
    const methods: Array<{
      eventName: string;
      handler: ShellHandler;
      isSpecific: boolean;
      targetId?: string;
    }> = [];

    const skip = new Set(['onBootstrap', 'onConnected', 'onDisconnected', 'onAttributeChanged', 'onEval']);
    const proto = Object.getPrototypeOf(this);

    for (const key of Object.getOwnPropertyNames(proto)) {
      if (!key.startsWith('on') || skip.has(key)) continue;
      const fn = this[key];
      if (typeof fn !== 'function') continue;

      const fromMatch = key.match(/^on(.+?)From(.+)$/);
      if (fromMatch) {
        methods.push({
          eventName: this._lcFirst(fromMatch[1]),
          handler: (fn as Function).bind(this) as ShellHandler,
          isSpecific: true,
          targetId: fromMatch[2],
        });
      } else {
        methods.push({
          eventName: this._lcFirst(key.slice(2)),
          handler: (fn as Function).bind(this) as ShellHandler,
          isSpecific: false,
        });
      }
    }
    return methods;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Autoview — Automatic View Child Mirroring
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Enable autoview: subscribe to tracked domain's $structureChanged events
   * and auto-spawn/remove matching child view components.
   *
   * Only activates when both `autoview` and `tracks` attributes are present.
   * Spec rules: R-AV1 through R-AV5 (FP-20c-2).
   */
  private _enableAutoView(): void {
    if (!this._context) return;

    // R-AV1: autoview only activates when tracks is also set
    if (!this.hasAttribute('autoview')) return;

    const tracksAttr = this.getAttribute('tracks');
    const ctor = this.constructor as typeof MatrixActorHtmlElement;
    const staticTracks = ctor.tracks;
    const firstTrack = tracksAttr?.split(',')[0]?.trim() ?? staticTracks?.[0];

    if (!firstTrack) return;

    const myMount = this._context.mount;
    const parentMount = myMount.split('.').slice(0, -1).join('.');
    const transport = this._context.transport;

    // Resolve tracked mount (try child mount first, then sibling)
    const trackedMount = myMount ? `${myMount}.${firstTrack}` : firstTrack;
    const trackedEventsTopic = TopicRouter.events(trackedMount);

    // Subscribe to $structureChanged events on the tracked domain
    const unsub = transport.subscribe(trackedEventsTopic, (payload: unknown) => {
      const envelope = payload as { op: string; payload: unknown };
      if (!envelope || envelope.op !== '$structureChanged') return;

      const data = envelope.payload as {
        added?: Array<{ id: string; type: string }>;
        removed?: Array<{ id: string }>;
      };

      // Spawn views for added children
      if (data.added) {
        for (const child of data.added) {
          this._spawnViewFor(child.id, child.type, trackedMount);
        }
      }

      // Remove views for removed children
      if (data.removed) {
        for (const child of data.removed) {
          this._removeViewFor(child.id);
        }
      }
    });
    this._addShellSub(unsub);

    // Also try sibling mount if different
    const siblingMount = parentMount ? `${parentMount}.${firstTrack}` : firstTrack;
    if (siblingMount !== trackedMount) {
      const siblingEventsTopic = TopicRouter.events(siblingMount);
      const unsub2 = transport.subscribe(siblingEventsTopic, (payload: unknown) => {
        const envelope = payload as { op: string; payload: unknown };
        if (!envelope || envelope.op !== '$structureChanged') return;

        const data = envelope.payload as {
          added?: Array<{ id: string; type: string }>;
          removed?: Array<{ id: string }>;
        };

        if (data.added) {
          for (const child of data.added) {
            this._spawnViewFor(child.id, child.type, siblingMount);
          }
        }

        if (data.removed) {
          for (const child of data.removed) {
            this._removeViewFor(child.id);
          }
        }
      });
      this._addShellSub(unsub2);
    }
  }

  /**
   * Spawn a view element for a domain child.
   * R-AV2: Convention is domain tag `X` → view tag `X-view`.
   * R-AV3: Missing view tag logs warning but doesn't throw.
   * R-AV5: View children are added to light DOM (external-owned).
   */
  private _spawnViewFor(childId: string, childType: string, trackedMount: string): void {
    const viewTag = this._resolveViewTag(childType);

    // R-AV3: Check if view tag is registered
    if (typeof customElements !== 'undefined' && !customElements.get(viewTag)) {
      this.warnKernel(
        `[MatrixActorHtmlElement] autoview: view tag '${viewTag}' not registered, skipping spawn for '${childId}'`
      );
      return;
    }

    // Don't duplicate if already spawned
    const existing = this.querySelector(`[data-autoview-id="${childId}"]`);
    if (existing) return;

    // Create view element
    const viewEl = document.createElement(viewTag);
    viewEl.setAttribute('tracks', childId);
    viewEl.setAttribute('data-autoview-id', childId);

    // R-AV5: Append to light DOM
    this.appendChild(viewEl);
  }

  /**
   * Remove an auto-spawned view child.
   */
  private _removeViewFor(childId: string): void {
    const viewEl = this.querySelector(`[data-autoview-id="${childId}"]`);
    if (viewEl) {
      viewEl.remove();
    }
  }

  /**
   * Resolve domain tag to view tag.
   * R-AV2: Convention: `X` → `X-view`. Custom mappings override.
   */
  protected _resolveViewTag(domainTag: string): string {
    // Check for custom mapping attribute
    const mapping = this.getAttribute('autoview-map');
    if (mapping) {
      try {
        const map = JSON.parse(mapping) as Record<string, string>;
        if (map[domainTag]) return map[domainTag];
      } catch {
        // Invalid JSON, fall through to convention
      }
    }

    return `${domainTag}-view`;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Child Wiring
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Wire up child shells in shadow DOM.
   * GAP 8 FIX: Only discovers DIRECT child shells — stops at shell boundaries.
   */
  private async _wireChildren(): Promise<void> {
    if (!this._context) return;
    const ctor = this.constructor as typeof MatrixActorHtmlElement;
    if (ctor._layoutOnly) return;

    // Find direct child shells (stop at shell boundaries)
    const childShells = this._getDirectChildShells();

    // Merge with pending children, deduplicate
    const allChildren = [...new Set([...childShells, ...this._pendingChildren])];
    this._pendingChildren = [];

    // Mount all children in parallel — each child's lifecycle ($join, render,
    // wire grandchildren) is independent, so no need to serialize.
    const mountPromises: Promise<void>[] = [];
    for (const child of allChildren) {
      const childName = (child.name || child.id || child.tagName).toLowerCase();
      const childContext = this._context.derive(childName);

      if (child._contextReceived) {
        this._subscribeToChildEvents(child, childContext);
        if (child._context?.mount !== childContext.mount) {
          mountPromises.push(child._receiveContext(childContext));
        }
        continue;
      }

      this._subscribeToChildEvents(child, childContext);
      mountPromises.push(child._receiveContext(childContext));
    }
    await Promise.all(mountPromises);
  }

  /**
   * Get direct child shells from shadow DOM.
   * Stops at shell boundaries — does NOT recurse into nested shells.
   * This prevents grandchild elements from being parented to grandparent.
   */
  _getDirectChildShells(): MatrixActorHtmlElement[] {
    if (!this.shadowRoot) return [];

    const result: MatrixActorHtmlElement[] = [];
    const seen = new Set<MatrixActorHtmlElement>();

    const asMatrixShell = (node: Node): MatrixActorHtmlElement | null => {
      const isLayoutOnly = (value: Element): boolean => {
        const ctor = value.constructor as unknown as { _layoutOnly?: unknown };
        return ctor._layoutOnly === true;
      };
      if (node instanceof MatrixActorHtmlElement) {
        if (isLayoutOnly(node)) return null;
        return node;
      }
      if (!(node instanceof Element)) {
        return null;
      }
      if (isLayoutOnly(node)) {
        return null;
      }
      const candidate = node as Element & {
        _receiveContext?: unknown;
        sendToChild?: unknown;
      };
      if (
        typeof candidate._receiveContext === 'function'
        && typeof candidate.sendToChild === 'function'
      ) {
        return candidate as unknown as MatrixActorHtmlElement;
      }
      return null;
    };

    const pushShell = (shell: MatrixActorHtmlElement) => {
      if (seen.has(shell)) return;
      seen.add(shell);
      result.push(shell);
    };

    const walk = (node: Node) => {
      for (const child of Array.from(node.childNodes)) {
        const childShell = asMatrixShell(child);
        if (childShell) {
          pushShell(childShell);
          // DO NOT recurse into child shells — they handle their own children
        } else if (
          child instanceof Element
          && child.tagName === 'SLOT'
          && typeof (child as Element & { assignedElements?: (options?: { flatten?: boolean }) => Element[] }).assignedElements === 'function'
        ) {
          const slot = child as Element & { assignedElements: (options?: { flatten?: boolean }) => Element[] };
          for (const assigned of slot.assignedElements({ flatten: true })) {
            const assignedShell = asMatrixShell(assigned);
            if (assignedShell) {
              pushShell(assignedShell);
            } else {
              walk(assigned);
            }
          }
        } else if (child instanceof Element) {
          // Recurse into non-shell elements (divs, slots, etc.)
          walk(child);
        }
      }
    };

    walk(this.shadowRoot);
    return result;
  }

  // ── Oracle snapshot support (FP-42) ─────────────────────────────────────────

  /**
   * Return the semantic state this component exposes to the oracle.
   * Override in subclasses to expose meaningful state.
   * Never include DOM refs, caches, or internal bookkeeping here.
   */
  getCognitiveState(): Record<string, unknown> {
    return {};
  }

  /**
   * Build an OracleTreeNode for this component and its children.
   * Children are discovered via _getDirectChildShells() (same-process, direct call).
   * The result is serialized into the oracle system prompt as s-expressions.
   */
  async snapshotTreeForOracle(): Promise<OracleSnapshotNode> {
    const ctor = this.constructor as typeof MatrixActorHtmlElement;

    const accepts: OracleSnapshotAcceptsEntry[] = Object.entries(
      (ctor.accepts ?? {}) as Record<string, Record<string, string> | null | undefined>,
    ).map(([op, schema]) => ({
      op,
      params: Object.entries(schema ?? {}).map(
        ([n, t]) => [n, String(t)] as [string, string],
      ),
    }));

    // Framework styling ops — available on ALL HTML actors, shown in oracle schemas
    // so the AI discovers them naturally alongside domain accepts.
    accepts.push(
      { op: '$tokens', params: [['tokens', 'Record<string,string>']] },
      { op: '$style',  params: [['props', 'Record<string,string>']] },
      { op: '$prompt', params: [['prompt', 'string'], ['sessionId', 'string?']] },
    );

    const childShells = this._getDirectChildShells();
    const children = await Promise.all(
      childShells.map(child => child.snapshotTreeForOracle()),
    );

    return {
      mount:          this._context?.mount ?? '',
      tag:            this.tagName.toLowerCase(),
      ...(ctor.description ? { description: ctor.description } : {}),
      accepts,
      emits:          Object.keys(ctor.emits ?? {}),
      tokens:         ctor.tokens ?? {},
      cognitiveState: this.getCognitiveState(),
      children,
    };
  }

  /**
   * Framework op handler: snapshot.tree.oracle.
   * Wired as a framework op so it's available on ALL components (Pattern A and B).
   * Any component can be oracle-snapshotted for the "any actor is thinkable" architecture.
   */
  async onSnapshotTreeOracle(_payload: Record<string, unknown>): Promise<unknown> {
    return this.snapshotTreeForOracle();
  }

  // ═══════════════════════════════════════════════════════════════════════
  // $prompt — Component-as-Canvas (any component is promptable)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Framework op handler: $prompt (ADR-022: Event-Driven Prompt Architecture).
   *
   * Send a natural language prompt to this component. The component builds its
   * own snapshot, forwards to the daemon, subscribes to agent events, and relays
   * them to its own $events stream. Returns an ACK immediately — the actual
   * result arrives as a $agentResponse event.
   *
   * The agent's tools are scoped to this component's subtree.
   */
  async on$prompt(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
    const prompt = typeof payload?.prompt === 'string' ? payload.prompt : '';
    if (!prompt) {
      return { ok: false, code: 'MISSING_PROMPT', message: '$prompt requires a prompt string' };
    }
    if (!this._context) {
      return { ok: false, code: 'NO_CONTEXT', message: 'Component not initialized' };
    }

    // Trace: log incoming payload fields so relay transformations are visible
    const incomingKeys = Object.keys(payload).join(',');
    this.infoKernel(`[on$prompt] relay at ${this._context.mount}: incoming keys=[${incomingKeys}], principalId=${payload.principalId ?? '(none)'}, sessionId=${payload.sessionId ?? '(none)'}`);

    // 1. Generate or accept requestId (ADR-020) and traceId (§7)
    const requestId = typeof payload.requestId === 'string'
      ? payload.requestId
      : `req-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
    const traceId = typeof payload.traceId === 'string' && payload.traceId
      ? payload.traceId
      : `trace-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;

    // 2. Build snapshot — component knows its own tree
    let snapshotTree: unknown;
    try {
      snapshotTree = await this.snapshotTreeForOracle();
    } catch (err) {
      this.warnKernel(`[on$Prompt] snapshot failed, proceeding without:`, err);
    }

    // 3. Resolve addressing
    const daemonRoot = this._resolveDaemonRoot();
    const llmBase = this._actor?.context?.getService<string>('llm-endpoint') ?? 'system.agents';
    const llmTarget = `${daemonRoot}/${llmBase}`;
    const callerRoot = this._resolveLocalRoot();
    const callerMount = this._context.mount;

    // 4. Resolve session
    const sessionId = (typeof payload.sessionId === 'string' ? payload.sessionId : null)
      ?? this._agentSessionId
      ?? undefined;

    // 5. Build $prompt payload
    const promptPayload: Record<string, unknown> = {
      prompt,
      requestId,
      traceId,
      ...(typeof payload.parentSpanId === 'string' ? { parentSpanId: payload.parentSpanId } : {}),
      callerRoot,
      callerRealm: callerRoot, // backward compat
      callerMount,
      ...(sessionId ? { sessionId } : {}),
      executionPolicy: payload.executionPolicy ?? {
        allowCommands: ['*'],
        denyCommands: [],
        maxToolCalls: 20,
      },
      ...(snapshotTree ? { snapshotTree, targetSnapshots: [snapshotTree] } : {}),
      // Forward principalId for session scoping (INV-ID-6)
      ...(typeof payload.principalId === 'string' ? { principalId: payload.principalId } : {}),
    };

    // Merge caller's snapshot into targetSnapshots for cross-context visibility.
    // When ChatApp retargets to FlowPad, it includes callerSnapshot so the
    // agent sees BOTH trees (this component's tree + the chat page's tree).
    if (payload.callerSnapshot && typeof payload.callerSnapshot === 'object') {
      const existing = Array.isArray(promptPayload.targetSnapshots) ? promptPayload.targetSnapshots : [];
      promptPayload.targetSnapshots = [...existing, payload.callerSnapshot];
    }

    // Trace: log outgoing payload fields so dropped/added fields are visible
    const outgoingKeys = Object.keys(promptPayload).join(',');
    this.infoKernel(`[on$prompt] relay at ${callerMount} → ${llmTarget}: outgoing keys=[${outgoingKeys}], principalId=${promptPayload.principalId ?? '(none)'}, traceId=${traceId}`);

    // ─── Blocking path (CLI callers + nested agent $prompt) ─────────────
    // Cross-root event subscriptions don't work (daemon events aren't
    // forwarded through the federation backbone). For blocking callers,
    // use RR to SystemLlm with blocking=true — SystemLlm has its own
    // blocking mechanism that waits for the child agent's $response.
    if (payload.blocking) {
      promptPayload.blocking = true;
      // Forward activityTo sinks from parent agent — child activity bubbles up the chain
      if (Array.isArray(payload.activityTo)) {
        promptPayload.activityTo = payload.activityTo;
      }
      try {
        const result = await RequestReply.execute(
          this._context, llmTarget, '$prompt', promptPayload,
          { timeoutMs: 300_000 },
        ) as Record<string, unknown>;

        // Track session for future non-blocking calls
        if (typeof result.sessionId === 'string' && result.sessionId) {
          this._agentSessionId = result.sessionId;
        }
        return { ...result, requestId };
      } catch (err) {
        return {
          ok: false,
          code: 'BLOCKING_TIMEOUT',
          message: err instanceof Error ? err.message : String(err),
          requestId,
          sessionId,
        };
      }
    }

    // ─── Non-blocking path (browser UI callers) ────────────────────────
    // Fire-and-forget to daemon. Activity arrives back via activityTo → $activity
    // on the CALLER's $inbox (cross-root safe — no local $events subscription).

    // 6. Pass through activityTo from the caller (e.g. ChatApp passes its own mount)
    if (Array.isArray(payload.activityTo)) {
      promptPayload.activityTo = payload.activityTo;
    }

    // 7. Fire-and-forget to daemon's $inbox
    RequestReply.sendToInbox(this._context, llmTarget, '$prompt', promptPayload);

    // Return ACK to caller immediately
    return { ok: true, accepted: true, requestId, sessionId: sessionId ?? this._agentSessionId };
  }

  /**
   * Framework op handler: $cancel (ADR-022).
   * Forwards cancel to the daemon agent for this component.
   */
  async on$Cancel(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
    if (!this._context) return { ok: false, code: 'NO_CONTEXT' };
    const requestId = typeof payload?.requestId === 'string' ? payload.requestId : undefined;
    const daemonRoot = this._resolveDaemonRoot();
    const callerRoot = this._resolveLocalRoot();
    const callerMount = this._context.mount;
    const agentChildSegment = `${callerRoot}.${callerMount}`;
    const llmBase = this._actor?.context?.getService<string>('llm-endpoint') ?? 'system.agents';
    const agentMount = `${daemonRoot}/${llmBase}.${agentChildSegment}`;
    this.infoKernel(`[on$Cancel] relay at ${callerMount} → ${agentMount}: requestId=${requestId ?? '(none)'}`);
    try {
      await RequestReply.execute(this._context, agentMount, '$cancel', { requestId }, { timeoutMs: 5_000 });
      return { ok: true, cancelled: true };
    } catch {
      return { ok: false, code: 'CANCEL_FAILED' };
    }
  }

  /**
   * Framework op handler: $activity (activityTo push delivery).
   * Receives ActivityFrame from daemon when a prompted agent emits events.
   *
   * Default behavior:
   * - Toggles `data-activity-active` / `data-activity-phase` attributes for CSS feedback
   * - Accumulates frames in `_activityFrames[]` (protected, subclass-readable)
   * - Calls lifecycle hooks: `onActivityStarted()`, `onActivityFrame()`, `onActivityEnded()`
   *
   * Subclasses override for custom activity handling (e.g. ChatApp renders progress).
   * When overriding, call `super.on$Activity(payload)` to preserve accumulation + hooks.
   */
  on$Activity(payload: ActivityFrame): void {
    const phase = payload?.phase;
    if (!phase) return;

    // Accumulate frame
    this._activityFrames.push(payload);

    // Lifecycle: activity started (first non-terminal frame)
    if (!this._activityActive && !isTerminalPhase(phase)) {
      this._activityActive = true;
      this.setAttribute('data-activity-active', '');
      this.onActivityStarted(payload);
    }

    // Update phase attribute for CSS targeting
    this.setAttribute('data-activity-phase', phase);

    // Per-frame hook
    this.onActivityFrame(payload);

    // Lifecycle: activity ended (terminal phase)
    if (isTerminalPhase(phase)) {
      const completedFrames = [...this._activityFrames];
      this._activityActive = false;
      this._activityFrames = [];
      this.removeAttribute('data-activity-active');
      this.removeAttribute('data-activity-phase');
      this.onActivityEnded(completedFrames);
    }
  }

  /**
   * Hook: called when activity starts (first non-terminal $activity frame received).
   * Override in subclasses for custom UI (e.g. show cancel button, pulse status dot).
   */
  protected onActivityStarted(_firstFrame: ActivityFrame): void {
    // Default: no-op. Subclasses override.
  }

  /**
   * Hook: called for every $activity frame during an active request.
   * Override in subclasses for real-time activity display.
   */
  protected onActivityFrame(_frame: ActivityFrame): void {
    // Default: no-op. Subclasses override.
  }

  /**
   * Hook: called when activity ends (terminal phase: done, error, cancelled).
   * Receives all accumulated frames from the completed request.
   * Override in subclasses for post-hoc summaries.
   */
  protected onActivityEnded(_frames: ActivityFrame[]): void {
    // Default: no-op. Subclasses override.
  }

  /**
   * Resolve the bus root from the closest matrix-dsl-host element.
   * Protected so subclasses (e.g. ChatApp) can override.
   */
  protected _resolveDaemonRoot(): string {
    const dslHost = this.closest('matrix-dsl-host') ?? document.querySelector('matrix-dsl-host');
    const busRoot = dslHost?.getAttribute('bus-root');
    if (busRoot && busRoot.trim().length > 0) {
      return busRoot.trim();
    }
    throw new Error('MatrixActorHtmlElement requires bus-root on matrix-dsl-host');
  }

  /** @deprecated Use _resolveDaemonRoot. */
  protected _resolveDaemonRealm(): string {
    return this._resolveDaemonRoot();
  }

  /**
   * Resolve the local root from context transport or session info.
   * Protected so subclasses (e.g. ChatApp) can override.
   */
  protected _resolveLocalRoot(): string {
    if (!this._context) return 'unknown';
    const transport = this._context.transport as unknown as { root?: string; realm?: string };
    if (transport?.root) return transport.root;
    if (transport?.realm) return transport.realm;
    const SESSION_INFO_KEY = Symbol.for('matrix.session.info');
    const sessionInfo = this._context.getService?.(SESSION_INFO_KEY) as { root?: string; realm?: string } | undefined;
    if (sessionInfo?.root) return sessionInfo.root;
    if (sessionInfo?.realm) return sessionInfo.realm;
    return 'unknown';
  }

  /** @deprecated Use _resolveLocalRoot. */
  protected _resolveLocalRealm(): string {
    return this._resolveLocalRoot();
  }

  /**
   * Subscribe to events from a child shell.
   * Uses handler cascade: declarative bindings → specific → generic.
   */
  private _subscribeToChildEvents(child: MatrixActorHtmlElement, childContext: IMatrixContext): void {
    if (!this._context) return;

    // Get child's emits from its static metadata
    const childCtor = child.constructor as typeof MatrixActorHtmlElement;
    let emitNames: string[];

    const ActorClass = childCtor.getActorClass?.();
    if (ActorClass) {
      emitNames = Object.keys(ActorClass.emits ?? {});
    } else {
      emitNames = Object.keys(childCtor.emits ?? {});
    }

    if (emitNames.length === 0) return;

    const childName = child.name || child.id;

    // Build dispatch table: event → handler
    const eventHandlers: Record<string, Function> = {};
    for (const event of emitNames) {
      const specificName = `on${toHandlerCase(event)}From${toHandlerCase(childName)}`;
      const specificHandler = this[specificName];
      const genericName = `on${toHandlerCase(event)}`;
      const genericHandler = this[genericName];

      if (typeof specificHandler === 'function') {
        eventHandlers[event] = specificHandler;
      } else if (typeof genericHandler === 'function') {
        eventHandlers[event] = genericHandler;
      }
    }

    if (Object.keys(eventHandlers).length === 0) return;

    // SINGLE subscription to child's $events topic
    const eventsTopic = TopicRouter.events(childContext.mount);
    const unsub = this._context.transport.subscribe(eventsTopic, (payload, meta) => {
      const envelope = payload as MxEnvelope;
      if (!envelope || !envelope.op) return;
      const handler = eventHandlers[envelope.op];
      if (handler) {
        try {
          handler.call(this, envelope.payload, meta);
        } catch (err) {
          this.errorKernel(`[MatrixActorHtmlElement] Error in handler for ${envelope.op}:`, err);
        }
      }
    });
    this._addShellSub(unsub);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Framework Styling Ops ($style / $tokens)
  //
  // Available on EVERY MatrixActorHtmlElement — base class level.
  // $tokens sets --mx-* CSS custom properties that cascade to children.
  // $style sets arbitrary inline CSS on the host element.
  // These are framework-level ops, like $introspect — zero opt-in required.
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * $style — Set arbitrary CSS properties on this element's host.
   * AI-addressable: send to any actor's $inbox.
   *
   * Payload: { props: Record<string, string> }
   * Example: { props: { 'background': 'red', '--custom-var': 'blue' } }
   */
  on$Style(payload: { props: Record<string, string> }): void {
    if (!payload?.props) return;
    for (const [prop, value] of Object.entries(payload.props)) {
      this.style.setProperty(prop, value);
    }
  }

  /**
   * $source-code — Return this component's source code via constructor.toString().
   * Works for all components: LLM-created (readable JS), built-in (compiled JS).
   */
  on$SourceCode(): { source: string; tag: string; mount: string } {
    return {
      source: this.constructor.toString(),
      tag: this.tagName.toLowerCase(),
      mount: this._context?.mount ?? '',
    };
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Factory: create child components from code strings
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * factory.create-from-code — DOM-aware override.
   *
   * Evals a JavaScript class string extending MatrixActorHtmlElement,
   * registers it as a custom element with a versioned tag name,
   * creates the DOM element, and mounts it on the bus via $join handshake.
   *
   * Subclasses override _insertFactoryElement() for custom DOM placement.
   */
  async onFactoryCreateFromCode(
    payload: { name: string; code: string }
  ): Promise<Record<string, unknown>> {
    const { name, code } = payload;
    if (!name || !code) {
      return { ok: false, error: 'factory.create-from-code requires name and code' };
    }
    if (!this._context) {
      return { ok: false, error: 'no context — component not initialized' };
    }

    let cleanName = name.replace(/^mx-/, '').replace(/-v\d+$/, '');

    // Security gate: check if dynamic compilation is allowed in this policy
    if (!this._context?.runtime.isDynamicCompilationAllowed?.()) {
      return { ok: false, error: 'Dynamic component compilation is DISABLED in this security policy.' };
    }

    // GAP-5 / ZD-9: Spawn policy check — SecurityRealm gates dynamic component creation.
    // Consults the context-cascaded SecurityRealm (injected by runtime, GAP-4).
    // Default policy allows all spawns — no behavioral change unless restrictive policy configured.
    const securityRealm = this._context?.securityRealm;
    if (securityRealm && typeof securityRealm.evaluateSpawn === 'function') {
      const spawnDecision = securityRealm.evaluateSpawn({
        componentType: cleanName,
        parentMount: this._context?.mount ?? 'unknown',
        slot: 'default',
        props: {},
        requestor: {
          type: 'component',
          id: this._context?.mount ?? 'unknown',
          trustLevel: 'user',
        },
      });
      if (!spawnDecision.allowed) {
        return { ok: false, error: `Spawn denied: ${spawnDecision.reason ?? 'policy violation'}` };
      }
    }

    // Versioned tag (custom elements can't be re-registered). The in-memory
    // version map can drift from the browser registry across restores/reloads,
    // so probe customElements too before choosing the next tag.
    let version = this._factoryVersions.get(cleanName) ?? 0;
    let tag = '';
    do {
      version += 1;
      tag = `mx-${cleanName}-v${version}`;
    } while (customElements.get(tag));
    this._factoryVersions.set(cleanName, version);

    rejectKnownBadPatterns(code);

    // Eval with MatrixActorHtmlElement in scope
    let ComponentClass: (CustomElementConstructor & {
      accepts?: Record<string, Record<string, never>>;
      template?: string;
      styles?: string;
      prototype: MatrixActorHtmlElement & {
        template?: string;
        styles?: string;
        render?: ReactiveRenderFn;
      };
    });
    try {
      ComponentClass = new Function(
        'MatrixActorHtmlElement', `return (${code})`
      )(MatrixActorHtmlElement) as (CustomElementConstructor & {
        accepts?: Record<string, Record<string, never>>;
        template?: string;
        styles?: string;
        prototype: MatrixActorHtmlElement & {
          template?: string;
          styles?: string;
          render?: ReactiveRenderFn;
        };
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return {
        ok: false,
        error: {
          code: 'FACTORY_COMPONENT_EVAL_FAILED',
          message: `Failed to evaluate component code for "${cleanName}".`,
          stage: 'eval',
          retryable: false,
          target: this._context?.mount ?? '',
          op: 'factory.create-from-code',
          name: cleanName,
          tag,
          cause: {
            code: err instanceof Error && err.name ? err.name : 'EVAL_ERROR',
            message,
          },
        },
      };
    }

    // Copy template/styles from prototype getter to static property
    // (_renderTemplate reads ctor.template, i.e. static — not instance getter)
    const proto = ComponentClass.prototype;
    const hasRender = typeof proto.render === 'function';
    if (!ComponentClass.template) {
      const desc = Object.getOwnPropertyDescriptor(proto, 'template');
      if (desc?.get) {
        try { ComponentClass.template = desc.get.call({}); } catch { /* ignore */ }
      }
    }
    let stylesStr = ComponentClass.styles;
    if (!stylesStr) {
      const desc = Object.getOwnPropertyDescriptor(proto, 'styles');
      if (desc?.get) {
        try { stylesStr = desc.get.call({}); } catch { /* ignore */ }
      }
    }
    if (stylesStr) {
      ComponentClass.styles = stylesStr;
    }
    if (stylesStr && ComponentClass.template && !hasRender) {
      ComponentClass.template = `<style>${stylesStr}</style>${ComponentClass.template}`;
    }

    // Auto-derive static accepts from onXxx methods if not explicitly set
    if (!ComponentClass.accepts || Object.keys(ComponentClass.accepts).length === 0) {
      const derived: Record<string, Record<string, never>> = {};
      for (const key of Object.getOwnPropertyNames(proto)) {
        if (key.startsWith('on') && key.length > 2 && typeof proto[key] === 'function') {
          if (key === 'onMounted') continue;
          const op = key[2].toLowerCase() + key.slice(3);
          derived[op] = {};
        }
      }
      ComponentClass.accepts = derived;
    }

    // Register custom element
    try {
      customElements.define(tag, ComponentClass);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return {
        ok: false,
        error: {
          code: 'CUSTOM_ELEMENT_DEFINE_CONFLICT',
          message: `Failed to register custom element "${tag}" for component "${cleanName}".`,
          stage: 'custom-element-register',
          retryable: false,
          target: this._context?.mount ?? '',
          op: 'factory.create-from-code',
          name: cleanName,
          tag,
          cause: {
            code: err instanceof Error && err.name ? err.name : 'DOM_EXCEPTION',
            message,
          },
        },
      };
    }

    // Create element, let subclass control DOM placement
    const mountSegment = `component-${cleanName}`;
    const el = document.createElement(tag);
    el.setAttribute('name', mountSegment);
    this._insertFactoryElement(el, cleanName);

    // Mount on bus → triggers $join handshake → auto-parents in ChildManager
    const childContext = this._context.derive(mountSegment);
    try {
      await (el as unknown as MatrixActorHtmlElement).mount(childContext as never);
      // After mount completes, template is in shadow DOM — call onMounted() hook
      const codeEl = el as unknown as { onMounted?: () => void };
      if (typeof codeEl.onMounted === 'function') {
        codeEl.onMounted();
      }
    } catch (err) {
      this.errorKernel(`[MatrixActorHtmlElement] Failed to mount factory component ${cleanName}:`, err);
      const message = err instanceof Error ? err.message : String(err);
      return {
        ok: false,
        error: {
          code: 'FACTORY_COMPONENT_MOUNT_FAILED',
          message: `Component "${cleanName}" was defined but failed to mount.`,
          stage: 'mount',
          retryable: false,
          target: childContext.mount,
          op: 'factory.create-from-code',
          name: cleanName,
          tag,
          cause: {
            code: err instanceof Error && err.name ? err.name : 'MOUNT_ERROR',
            message,
          },
        },
      };
    }

    return { ok: true, tag, name: cleanName, mount: childContext.mount };
  }

  /**
   * Insert a factory-created element into the DOM.
   * Override in subclass for custom placement (e.g., ChatMessageList wraps in a message div).
   */
  protected _insertFactoryElement(el: HTMLElement, _name: string): void {
    this.shadowRoot?.appendChild(el);
  }

  /**
   * factory.load — Load a saved component from ComponentStoreActor.
   *
   * Browser-side: resolves the daemon root, invokes component.get-source
   * cross-root on system.component-store, then delegates to
   * onFactoryCreateFromCode for DOM eval + mount.
   */
  async onFactoryLoad(
    payload: { name: string }
  ): Promise<Record<string, unknown>> {
    const { name } = payload ?? {};
    if (!name) {
      return { ok: false, error: 'factory.load requires name' };
    }
    if (!this._context) {
      return { ok: false, error: 'no context — component not initialized' };
    }

    try {
      const daemonRoot = this._resolveDaemonRoot();
      const storeBase = this._actor?.context?.getService<string>('component-store-endpoint') ?? 'system.component-store';
      const storeMount = `${daemonRoot}/${storeBase}`;

      const result = await RequestReply.execute(
        this._context, storeMount, 'component.get-source', { name },
        { timeoutMs: 10_000 },
      ) as Record<string, unknown>;

      if (result.error) {
        return { ok: false, error: result.error };
      }

      const source = result.source as string;
      if (!source) {
        return { ok: false, error: `No source returned for component "${name}"` };
      }

      // Delegate to the universal pipeline (DOM version)
      return this.onFactoryCreateFromCode({ name, code: source });
    } catch (err) {
      return { ok: false, error: err instanceof Error ? err.message : String(err) };
    }
  }

  /**
   * factory.create-from-url — Fetch source code from a URL, delegate to factory.create-from-code.
   *
   * Browser-only. The URL must return a raw class string (not an ES module).
   * Payload: { name, url }
   */
  async onFactoryCreateFromUrl(
    payload: { name: string; url: string }
  ): Promise<Record<string, unknown>> {
    const { name, url } = payload ?? {};
    if (!name || !url) {
      return { ok: false, error: 'factory.create-from-url requires name and url' };
    }

    try {
      const response = await fetch(url);
      if (!response.ok) {
        return { ok: false, error: `fetch failed: ${response.status} ${response.statusText}` };
      }
      const code = await response.text();
      return this.onFactoryCreateFromCode({ name, code });
    } catch (err) {
      return { ok: false, error: err instanceof Error ? err.message : String(err) };
    }
  }

  /**
   * component.save — Extract source from a running child and persist it.
   *
   * Browser-side: invokes $source-code on the child, then sends
   * component.persist cross-root to system.component-store.
   */
  async onComponentSave(
    payload: { mount: string; name?: string; description?: string }
  ): Promise<Record<string, unknown>> {
    const { mount } = payload ?? {};
    if (!mount) {
      return { ok: false, error: 'component.save requires mount' };
    }
    if (!this._context) {
      return { ok: false, error: 'no context — component not initialized' };
    }

    try {
      // Get source code from the running component
      const sourceResult = await RequestReply.execute(
        this._context, mount, '$source-code', {},
        { timeoutMs: 5_000 },
      ) as Record<string, unknown>;

      const source = (sourceResult?.source as string) ?? (typeof sourceResult === 'string' ? sourceResult : null);
      if (!source) {
        return { ok: false, error: `No source returned from $source-code at ${mount}` };
      }

      // Introspect the live component to capture full metadata (accepts, emits, tracks)
      let accepts: Array<{ op: string; params?: Record<string, unknown> }> = [];
      let emits: string[] = [];
      let tracks: string[] = [];
      let liveDescription = '';
      try {
        const intro = await RequestReply.execute(
          this._context, mount, '$introspect', {},
          { timeoutMs: 3_000 },
        ) as Record<string, unknown>;
        // Extract accepts ops (filter out framework ops)
        const frameworkPrefixes = ['$', 'factory.', 'component.', 'snapshot.'];
        const rawAccepts = (intro.accepts ?? intro.acceptsSchema) as string[] | Record<string, unknown> | undefined;
        if (Array.isArray(rawAccepts)) {
          accepts = rawAccepts
            .filter((op: string) => !frameworkPrefixes.some(p => op.startsWith(p)))
            .map((op: string) => ({ op }));
        } else if (rawAccepts && typeof rawAccepts === 'object') {
          accepts = Object.keys(rawAccepts)
            .filter(op => !frameworkPrefixes.some(p => op.startsWith(p)))
            .map(op => ({ op, params: (rawAccepts as Record<string, unknown>)[op] as Record<string, unknown> | undefined }));
        }
        // Extract emits
        const rawEmits = intro.emits as string[] | Record<string, unknown> | undefined;
        if (Array.isArray(rawEmits)) emits = rawEmits;
        else if (rawEmits && typeof rawEmits === 'object') emits = Object.keys(rawEmits);
        // Extract tracks from static field (not in $introspect — read from constructor)
        const rawTracks = intro.tracks as string[] | undefined;
        if (Array.isArray(rawTracks)) tracks = rawTracks;
        liveDescription = (intro.description as string) ?? '';
      } catch { /* introspection failed — save without metadata */ }

      // Derive name from mount if not provided
      const mountSegment = mount.split('.').pop() ?? mount;
      const componentName = payload.name ?? mountSegment.replace(/^component-/, '');

      // Persist via ComponentStoreActor (cross-root to daemon).
      // Merge logic lives in ComponentPersistenceService.writeComponent() —
      // if incoming accepts/emits/tracks are empty, existing manifest data is preserved.
      const daemonRoot = this._resolveDaemonRoot();
      const storeBase2 = this._actor?.context?.getService<string>('component-store-endpoint') ?? 'system.component-store';
      const storeMount = `${daemonRoot}/${storeBase2}`;

      const persistResult = await RequestReply.execute(
        this._context, storeMount, 'component.persist',
        {
          name: componentName,
          source,
          surface: 'dom',
          accepts,
          emits,
          tracks,
          description: payload.description ?? liveDescription,
        },
        { timeoutMs: 10_000 },
      ) as Record<string, unknown>;

      return persistResult;
    } catch (err) {
      return { ok: false, error: err instanceof Error ? err.message : String(err) };
    }
  }

  /**
   * $tokens — Set Matrix theme tokens (--mx-* CSS custom properties) on this element.
   * These cascade to all children via CSS custom property inheritance,
   * piercing shadow DOM boundaries automatically.
   *
   * Payload: { tokens: Record<string, string> }
   * Example: { tokens: { 'color-primary': '#e11d48' } } → sets --mx-color-primary
   */
  on$Tokens(payload: { tokens: Record<string, string> }): void {
    if (!payload?.tokens) return;
    for (const [token, value] of Object.entries(payload.tokens)) {
      this.style.setProperty(`--mx-${token}`, value);
    }
    // If this is the top-level shell (no parent context), also propagate
    // color-surface to the document body so the page background matches.
    if (payload.tokens['color-surface'] && !this.closest('matrix-dsl-host')?.parentElement?.closest('[data-mx-context]')) {
      document.documentElement.style.setProperty('--mx-color-surface', payload.tokens['color-surface']);
    }
    // Broadcast tokens to all descendant shells (traversing shadow DOM).
    // CSS custom properties should cascade through shadow DOM boundaries,
    // but deeply nested shadow DOMs (mx-split etc.) can break inheritance.
    // This ensures every component gets the tokens explicitly — like React's
    // ThemeProvider propagation pattern.
    this._broadcastTokensToDescendants(payload);
  }

  /**
   * Walk the DOM tree (including shadow roots) and forward $tokens
   * to every descendant MatrixActorHtmlElement.
   */
  private _broadcastTokensToDescendants(payload: { tokens: Record<string, string> }): void {
    const walk = (root: Element | ShadowRoot): void => {
      const children = root instanceof ShadowRoot ? root.children : root.children;
      for (let i = 0; i < children.length; i++) {
        const child = children[i];
        if (child instanceof MatrixActorHtmlElement && child !== this) {
          // Set CSS vars directly — don't re-publish via transport to avoid
          // infinite loops and unnecessary transport traffic.
          for (const [token, value] of Object.entries(payload.tokens)) {
            child.style.setProperty(`--mx-${token}`, value);
          }
          // Continue walking into this child's shadow DOM
          if (child.shadowRoot) {
            walk(child.shadowRoot);
          }
        } else {
          // Not a MatrixActorHtmlElement — keep walking
          if ((child as Element).shadowRoot) {
            walk((child as Element).shadowRoot!);
          }
          if (child.children.length > 0) {
            walk(child);
          }
        }
      }
    };
    // Walk our own shadow DOM first, then light DOM children
    if (this.shadowRoot) {
      walk(this.shadowRoot);
    }
    walk(this);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Dynamic Child Management
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Subscribe to lifecycle events for dynamic children.
   */
  private _subscribeToDynamicChildren(): void {
    if (!this._context) return;

    const mount = this._context.mount;

    const eventsTopic = TopicRouter.events(mount);
    const unsubLifecycle = this._context.transport.subscribe(
      eventsTopic,
      (payload: unknown) => {
        const envelope = payload as MxEnvelope;
        if (!envelope?.op) return;
        if (envelope.op === 'lifecycle.childAdded') {
          this._handleChildAdded(envelope.payload as IChildAddedPayload);
        } else if (envelope.op === 'lifecycle.childRemoved') {
          this._handleChildRemoved(envelope.payload as IChildRemovedPayload);
        }
      }
    );
    this._addShellSub(unsubLifecycle);
  }

  /**
   * Handle a dynamically added child by creating a DOM element.
   */
  protected async _handleChildAdded(payload: IChildAddedPayload): Promise<void> {
    if (!this.shadowRoot || !this._context) return;

    const ctor = this.constructor as typeof MatrixActorHtmlElement;
    const tag = payload.shellTag || ctor.getChildTag(payload.componentClass);

    if (!tag) return;

    const slotEl = this.shadowRoot.querySelector(`[data-slot="${payload.slot}"]`);
    if (!slotEl) {
      this.warnKernel(`[MatrixActorHtmlElement] Slot "${payload.slot}" not found for child "${payload.id}"`);
      return;
    }

    // Guard against duplicates
    const existingChild = slotEl.querySelector(`#${CSS.escape(payload.id)}`);
    if (existingChild) return;

    const childEl = document.createElement(tag) as ContextualChildElement;
    childEl.id = payload.id;

    if (payload.props) {
      childEl._initialProps = payload.props;
    }

    const childContext = this._context.derive(payload.id);
    if (typeof childEl.setContext === 'function') {
      childEl.setContext(childContext);
    }

    slotEl.appendChild(childEl);

    await customElements.whenDefined(tag);

    if (typeof childEl.setContext !== 'function' && typeof childEl._receiveContext === 'function') {
      await childEl._receiveContext(childContext);
    }
  }

  /**
   * Handle a dynamically removed child by removing its DOM element.
   */
  protected _handleChildRemoved(payload: IChildRemovedPayload): void {
    if (!this.shadowRoot) return;

    const slotEl = this.shadowRoot.querySelector(`[data-slot="${payload.slot}"]`);
    if (!slotEl) return;

    const childEl = slotEl.querySelector(`#${CSS.escape(payload.id)}`);
    if (childEl) {
      childEl.remove();
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Deep Bindings
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Wire bindings from the template to ANY descendant's events.
   * Delegates to internal actor if available, otherwise wires on element.
   */
  _wireDeepBindings(bindings: DeepBindingDescriptor[]): void {
    if (!this._context) return;

    // Group bindings by descendant path for single $events subscription per descendant
    const byPath = new Map<string, DeepBindingDescriptor[]>();
    for (const binding of bindings) {
      const list = byPath.get(binding.relativePath) ?? [];
      list.push(binding);
      byPath.set(binding.relativePath, list);
    }

    for (const [relativePath, pathBindings] of byPath) {
      const descendantMount = this._context.mount
        ? `${this._context.mount}.${relativePath}`
        : relativePath;

      // Build dispatch table for this descendant
      const bindingHandlers: Record<string, Function> = {};
      for (const binding of pathBindings) {
        const handler = this[binding.handler] ?? this._actor?.[binding.handler];
        if (typeof handler === 'function') {
          bindingHandlers[binding.event] = handler;
        }
      }

      if (Object.keys(bindingHandlers).length === 0) continue;

      // SINGLE subscription to descendant's $events
      const eventsTopic = TopicRouter.events(descendantMount);
      const unsub = this._context.transport.subscribe(eventsTopic, (payload, meta) => {
        const envelope = payload as MxEnvelope;
        if (!envelope || !envelope.op) return;
        const handler = bindingHandlers[envelope.op];
        if (typeof handler === 'function') {
          handler.call(this, envelope.payload, meta);
        }
      });
      this._addShellSub(unsub);
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Public API
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Send a command to a child by name.
   *
   * Publishes MxEnvelope to child's $inbox + legacy per-op (removed in 20b-6).
   */
  sendToChild(childName: string, command: string, payload?: unknown): void {
    if (!this._context) {
      throw new Error('[MatrixActorHtmlElement] Cannot send before context received');
    }
    const name = childName.toLowerCase();
    const childMount = this._context.mount ? `${this._context.mount}.${name}` : name;
    const transport = this._context.transport;

    const envelope: MxEnvelope = {
      op: command,
      payload: payload ?? {},
      from: this._context.mount,
    };
    transport.publish(TopicRouter.inbox(childMount), envelope);
  }

  /**
   * Send a command to this element's own actor via the transport bus.
   * CQRS pattern: shell → actor via bus, not direct method calls.
   */
  sendToSelf(command: string, payload?: unknown): void {
    if (!this._context) {
      throw new Error('[MatrixActorHtmlElement] Cannot send before context received');
    }
    const mount = this._context.mount;
    const transport = this._context.transport;

    const envelope: MxEnvelope = {
      op: command,
      payload: payload ?? {},
      from: mount,
    };
    transport.publish(TopicRouter.inbox(mount), envelope);
  }

  /**
   * Emit an event from this element via MxEnvelope to $events.
   */
  emit(event: string, payload?: unknown): void {
    if (!this._context) {
      throw new Error('[MatrixActorHtmlElement] Cannot emit before context received');
    }

    const mount = this._context.mount;
    const transport = this._context.transport;

    const envelope: MxEnvelope = {
      op: event,
      payload: payload ?? {},
      from: mount,
    };
    transport.publish(TopicRouter.events(mount), envelope);
  }

  /**
   * Publish a fact to the shared fact plane via the backing actor.
   */
  async publishFact(domain: string, data: Record<string, unknown>): Promise<number> {
    if (!this._actor) {
      throw new Error('[MatrixActorHtmlElement] Cannot publishFact before mount');
    }
    return this._actor.publishFact(domain, data);
  }

  /**
   * Read the latest fact snapshot from the shared fact plane.
   */
  async readFact(domain: string): Promise<{ data: unknown; revision: number } | null> {
    if (!this._actor) {
      throw new Error('[MatrixActorHtmlElement] Cannot readFact before mount');
    }
    return this._actor.readFact(domain);
  }

  /**
   * Subscribe to fact updates from the shared fact plane.
   */
  subscribeFact(domain: string, handler: (data: unknown) => void): () => void {
    if (!this._actor) {
      throw new Error('[MatrixActorHtmlElement] Cannot subscribeFact before mount');
    }
    return this._actor.subscribeFact(domain, handler);
  }

  /**
   * Send a command with two calling conventions:
   * 1. (target, command, data) — to child or relative path
   * 2. (command, data) — to first tracked domain component
   */
  sendCommand(toOrCmd: string, cmdOrData?: string | unknown, data?: unknown): void {
    if (!this._context) {
      throw new Error('[MatrixActorHtmlElement] Cannot sendCommand before context received');
    }

    if (typeof cmdOrData === 'string') {
      // 3-arg: (target, command, data) — delegate to actor
      if (this._actor) {
        this._actor.sendCommand(toOrCmd, cmdOrData, data);
      }
    } else {
      // 2-arg: (command, data) — send to first tracked component
      const ctor = this.constructor as typeof MatrixActorHtmlElement;
      const tracksAttr = this.getAttribute('tracks');
      const staticTracks = ctor.tracks;
      const firstTrack = tracksAttr?.split(',')[0]?.trim() ?? staticTracks?.[0];

      if (!firstTrack) {
        throw new Error('[MatrixActorHtmlElement] 2-arg sendCommand requires tracks');
      }

      const parentMount = this._context.mount.split('.').slice(0, -1).join('.');
      const trackedMount = parentMount ? `${parentMount}.${firstTrack}` : firstTrack;
      const inboxTopic = TopicRouter.inbox(trackedMount);
      const envelope: MxEnvelope = { op: toOrCmd, payload: cmdOrData ?? {} };
      this._context.transport.publish(inboxTopic, envelope);
    }
  }

  /**
   * Forward a command to the remote mount via transport (proxy mode).
   */
  protected _forwardToRemote(command: string, payload?: unknown): void {
    if (!this._remoteMount || !this._context) {
      this.warnKernel('[MatrixActorHtmlElement] Cannot forward - not in proxy mode or no context');
      return;
    }
    const inboxTopic = TopicRouter.inbox(this._remoteMount);
    const envelope: MxEnvelope = { op: command, payload: payload ?? {} };
    this._context.transport.publish(inboxTopic, envelope);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Lifecycle hooks
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Bootstrap hook — called after context is ready.
   * Override in subclass for initialization logic.
   */
  protected async onBootstrap(): Promise<void> {
    // Template method — subclasses override
  }

  /**
   * Context updated hook — called after context change or remount.
   * Override in subclass for context-specific subscriptions.
   */
  protected async _onContextUpdated(_context: IMatrixContext): Promise<void> {
    // Template method — subclasses override
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Utility Methods
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Find the nearest parent shell.
   * Traverses up through shadow DOM boundaries via ShadowRoot.host.
   */
  private _findParentShell(): MatrixActorHtmlElement | null {
    let current: Element | null = this.parentElement;

    while (current) {
      if (current instanceof MatrixActorHtmlElement) {
        // Skip layout-only components (e.g., mx-split) — they are actors but
        // don't parent the children in their light DOM slots. This preserves
        // mount paths when layout containers are inserted into the tree.
        const ctor = current.constructor as typeof MatrixActorHtmlElement;
        if (ctor._layoutOnly) {
          current = current.parentElement;
          continue;
        }
        return current;
      }
      // Shadow DOM boundary traversal
      if (current.parentElement === null) {
        const root = current.getRootNode();
        if (root && (root as ShadowRoot).host) {
          current = (root as ShadowRoot).host as Element;
          continue;
        }
      }
      current = current.parentElement;
    }

    return null;
  }

  private _isExplicitExternalHost(parentShell: MatrixActorHtmlElement): boolean {
    const tagName = parentShell.tagName.toLowerCase();
    return tagName === 'mx-web-module-host';
  }

  private _isDirectChildOfMatrixDslHost(): boolean {
    return this.parentElement?.tagName.toLowerCase() === 'matrix-dsl-host';
  }

  /**
   * Bridge DOM attributes to actor.
   */
  private _bridgeAttributes(): void {
    if (!this._actor) return;

    const actorRecord = this._actor as unknown as Record<string, unknown>;
    for (const attr of Array.from(this.attributes)) {
      const handlerName = `on${toHandlerCase(attr.name)}Changed`;
      const handler = actorRecord[handlerName];
      if (typeof handler === 'function') {
        handler.call(this._actor, attr.value, null);
      }
    }
  }

  /** Start orphan detection timer. */
  private _startOrphanDetection(): void {
    const timeout = (this.constructor as typeof MatrixActorHtmlElement).contextTimeoutMs;

    this._orphanTimeout = setTimeout(() => {
      if (!this._contextReceived) {
        this.warnKernel(
          `[MatrixActorHtmlElement] ${this.tagName.toLowerCase()} has not received context after ${timeout}ms.`
        );
      }
    }, timeout);
  }

  /** Clear orphan detection timeout. */
  private _clearOrphanTimeout(): void {
    if (this._orphanTimeout) {
      clearTimeout(this._orphanTimeout);
      this._orphanTimeout = undefined;
    }
  }

  /** Clear shell-level subscriptions. */
  private _clearShellSubs(): void {
    for (const unsub of this._shellSubs) {
      try { unsub(); } catch { }
    }
    this._shellSubs = [];
  }

  /** Clear fact-plane subscriptions and invalidate pending hydration work. */
  private _clearFactSubs(): void {
    this._factEpoch += 1;
    for (const unsub of this._factUnsubs) {
      try { unsub(); } catch { }
    }
    this._factUnsubs = [];
  }

  /** Add a shell-level subscription for automatic cleanup. */
  protected _addShellSub(unsub: () => void): void {
    this._shellSubs.push(unsub);
  }

  /** Lowercase first character. */
  private _lcFirst(s: string): string {
    if (!s) return '';
    return s.charAt(0).toLowerCase() + s.slice(1);
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// GLOBAL EXPOSURE FOR DYNAMIC COMPONENTS
// ═══════════════════════════════════════════════════════════════════════════
if (typeof globalThis !== 'undefined') {
  (globalThis as unknown as Record<string, unknown>).__MatrixActorHtmlElement = MatrixActorHtmlElement;
}
